Merge remote-tracking branch 'origin/2.3-gae'

Conflicts:
	build.xml
diff --git a/.project b/.project
index ccb2812..fcff841 100644
--- a/.project
+++ b/.project
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <projectDescription>
-	<name>FreeMarker-2.3-gae</name>
+	<name>FreeMarker-master</name>
 	<comment></comment>
 	<projects>
 	</projects>
@@ -29,7 +29,7 @@
 	</natures>
 	<filteredResources>
 		<filter>
-			<id>0</id>
+			<id>1416528675344</id>
 			<name></name>
 			<type>26</type>
 			<matcher>
diff --git a/build.xml b/build.xml
index 08024a0..3bb44e5 100644
--- a/build.xml
+++ b/build.xml
@@ -91,7 +91,7 @@
   <filter token="version" value="${version}" />
   
   <property name="dist.dir" value="build/dist" />
-  <property name="dist.archiveBaseName" value="apache-freemarker-gae" />
+  <property name="dist.archiveBaseName" value="apache-freemarker" />
   <property name="dist.bin.dir" value="${dist.dir}/bin/${dist.archiveBaseName}" />
   <property name="dist.src.dir" value="${dist.dir}/src/${dist.archiveBaseName}-src" />
   
@@ -783,7 +783,7 @@
   </parent>
   
   <groupId>org.freemarker</groupId>
-  <artifactId>freemarker-gae</artifactId>
+  <artifactId>freemarker</artifactId>
   <version>${mavenVersion}</version>
   
   <packaging>jar</packaging>
diff --git a/osgi.bnd b/osgi.bnd
index 9cc8cd8..412055e 100644
--- a/osgi.bnd
+++ b/osgi.bnd
@@ -49,13 +49,12 @@
 # This is needed for "a.class.from.another.Bundle"?new() to work.
 DynamicImport-Package: *
 
-# The required minimum is 1.4, but we utilize 1.5 if available.
+# Use "J2SE-<utilized-version>, J2SE-<minimum-version>" (i.e., highest to lowest).
 # See also: http://wiki.eclipse.org/Execution_Environments, "Compiling
 # against more than is required"
-Bundle-RequiredExecutionEnvironment: J2SE-1.5, J2SE-1.4
+Bundle-RequiredExecutionEnvironment: J2SE-1.5
 
 # Non-OSGi meta:
-Main-Class: freemarker.core.CommandLine
 Extension-name: FreeMarker
 Specification-Title: FreeMarker
 Specification-Version: ${versionForMf}
diff --git a/src/main/java/freemarker/cache/GetLastModifiedException.java b/src/main/java/freemarker/cache/GetLastModifiedException.java
new file mode 100644
index 0000000..1f369f5
--- /dev/null
+++ b/src/main/java/freemarker/cache/GetLastModifiedException.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.cache;
+
+import java.io.IOException;
+
+
+/**
+ * Used be {@link TemplateLoader#getLastModified(Object)} to indicate an error getting the last modification date. That
+ * should be just an {@link IOException}, but due to backward compatibility constraints that wasn't possible;
+ * {@link TemplateLoader#getLastModified(Object)} doesn't allow throwing checked exception.
+ * 
+ * @since 2.4.0
+ */
+public class GetLastModifiedException extends RuntimeException {
+
+    public GetLastModifiedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public GetLastModifiedException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/main/java/freemarker/cache/TemplateCache.java b/src/main/java/freemarker/cache/TemplateCache.java
index 4f62d05..a357500 100644
--- a/src/main/java/freemarker/cache/TemplateCache.java
+++ b/src/main/java/freemarker/cache/TemplateCache.java
@@ -436,7 +436,7 @@
             
             lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified;            
             Template template = loadTemplate(
-                    templateLoader, source,
+                    source,
                     name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,
                     encoding, parseAsFTL);
             cachedTemplate.templateOrException = template;
@@ -520,7 +520,7 @@
     }
 
     private Template loadTemplate(
-            final TemplateLoader templateLoader, final Object source,
+            final Object source,
             final String name, final String sourceName, Locale locale, final Object customLookupCondition,
             String initialEncoding, final boolean parseAsFTL) throws IOException {
         final TemplateConfiguration tc;
@@ -701,7 +701,9 @@
                     storage.remove(tk);
                 }
             }
-            LOG.debug(debugName + " was removed from the cache, if it was there");
+            if (debug) {
+                LOG.debug(debugName + " was removed from the cache, if it was there");
+            }
         }
     }
 
@@ -875,7 +877,7 @@
 
     /**
      * This class holds the cached template and associated information
-     * (the source object, and the last-checked and last-modified timestamps).
+     * (the source object (already closed), and the last-checked and last-modified time stamps).
      * It is used as the value in the cached templates map. Note: this class
      * is Serializable to allow custom 3rd party CacheStorage implementations 
      * to serialize/replicate them (see tracker issue #1926150); FreeMarker 
diff --git a/src/main/java/freemarker/cache/TemplateLoader.java b/src/main/java/freemarker/cache/TemplateLoader.java
index bab7cae..a545771 100644
--- a/src/main/java/freemarker/cache/TemplateLoader.java
+++ b/src/main/java/freemarker/cache/TemplateLoader.java
@@ -48,7 +48,7 @@
  * by the {@link TemplateCache}, and templates are get via the {@link TemplateCache} API-s.
  */
 public interface TemplateLoader {
-	
+        
     /**
      * Finds the template in the backing storage and returns an object that identifies the storage location where the
      * template can be loaded from. See the return value for more information.
@@ -92,16 +92,25 @@
     throws IOException;
         
     /**
-     * Returns the time of last modification of the specified template source. This method is called after
-     * <code>findTemplateSource()</code>.
+     * Returns the time of last modification of the specified template source, if the backing storage mechanism supports
+     * that.
      * 
      * @param templateSource
      *            an object representing a template source, obtained through a prior call to
      *            {@link #findTemplateSource(String)}. This must be an object on which
      *            {@link TemplateLoader#closeTemplateSource(Object)} wasn't applied yet.
-     * @return the time of last modification of the specified template source, or -1 if the time is not known.
+     * @return The time of last modification of the specified template source, or -1 if the time is not known. In
+     *         principle, -1 should be only returned if the backing storage doesn't store last modification times, not
+     *         when there was an error during getting the last modification time (then you should throw
+     *         {@link GetLastModifiedException}). However, Java's {@code File} and {@code URL} API can't differentiate
+     *         between these two cases, so for {@link TemplateLoader} based on those -1 is also used when an error has
+     *         occurred.
+     * 
+     * @throws GetLastModifiedException
+     *             If there was an error when reading the last modification date (since 2.4.0). Note that for backward
+     *             compatibility, this is an unchecked exception, and is not an {@link IOException}.
      */
-    public long getLastModified(Object templateSource);
+    public long getLastModified(Object templateSource) throws GetLastModifiedException;
     
     /**
      * Returns the character stream of a template represented by the specified template source. This method is possibly
diff --git a/src/main/java/freemarker/core/BodyInstruction.java b/src/main/java/freemarker/core/BodyInstruction.java
index 0c82841..db7527b 100644
--- a/src/main/java/freemarker/core/BodyInstruction.java
+++ b/src/main/java/freemarker/core/BodyInstruction.java
@@ -120,7 +120,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..1855c08
--- /dev/null
+++ b/src/main/java/freemarker/core/BoundCallable.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package 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 the {@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
+    TemplateElement[] accept(Environment env) throws TemplateException, IOException {
+        return 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);
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return unboundCallable.isNestedBlockRepeater();
+    }
+    
+}
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index ab796b0..a9aba7b 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -354,7 +354,7 @@
                 }
             }
                 
-            throw new ParseException(buf.toString(), null, keyTk);
+            throw new ParseException(buf.toString(), (UnboundTemplate) null, keyTk);
         }
         
         while (bi instanceof ICIChainMember
diff --git a/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java b/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
index 1820d98..5000f28 100644
--- a/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
+++ b/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
@@ -84,7 +84,7 @@
 
     protected ParseException newArgumentCountException(String ordinalityDesc, Token openParen, Token closeParen) {
         return new ParseException(
-                "?" + key + "(...) " + ordinalityDesc + " parameters", this.getTemplate(),
+                "?" + key + "(...) " + ordinalityDesc + " parameters", this.getUnboundTemplate(),
                 openParen.beginLine, openParen.beginColumn,
                 closeParen.endLine, closeParen.endColumn);
     }
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index 81bcf59..d70d356 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -334,8 +334,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;
         }
     }
@@ -386,7 +387,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;
         }
     }
@@ -469,13 +470,13 @@
         @Override
         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 3d21c0a..355be23 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
@@ -22,7 +22,6 @@
 import java.io.StringReader;
 
 import freemarker.template.SimpleNumber;
-import freemarker.template.Template;
 import freemarker.template.TemplateBooleanModel;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateModel;
@@ -57,7 +56,7 @@
         }
         
         TemplateModel calculateResult(String s, Environment env) throws TemplateException {
-            Template parentTemplate = getTemplate();
+            UnboundTemplate parentTemplate = getUnboundTemplate();
             
             Expression exp = null;
             try {
@@ -77,7 +76,7 @@
                     }
                     
                     FMParser parser = new FMParser(
-                            parentTemplate, false, tkMan, pCfg);
+                            parentTemplate, false, tkMan, null, pCfg);
                     
                     exp = parser.Expression();
                 } catch (TokenMgrError e) {
diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java
index 5b0f4d2..807b26b 100644
--- a/src/main/java/freemarker/core/BuiltinVariable.java
+++ b/src/main/java/freemarker/core/BuiltinVariable.java
@@ -73,6 +73,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[] {
         AUTO_ESC_CC,
         AUTO_ESC,
@@ -160,7 +162,7 @@
                     sb.append(correctName);
                 }
             }
-            throw new ParseException(sb.toString(), null, nameTk);
+            throw new ParseException(sb.toString(), (UnboundTemplate) null, nameTk);
         }
         
         this.name = name.intern();
@@ -181,7 +183,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 || name == DATA_MODEL_CC) {
@@ -217,7 +219,7 @@
             return SimpleScalar.newInstanceOrNull(env.getCurrentTemplate().getName());
         }
         if (name == PASS) {
-            return Macro.DO_NOTHING_MACRO;
+            return PASS_VALUE;
         }
         if (name == OUTPUT_ENCODING || name == OUTPUT_ENCODING_CC) {
             String s = env.getOutputEncoding();
diff --git a/src/main/java/freemarker/core/CallableInvocationContext.java b/src/main/java/freemarker/core/CallableInvocationContext.java
new file mode 100644
index 0000000..d2e204b
--- /dev/null
+++ b/src/main/java/freemarker/core/CallableInvocationContext.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+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[] nestedContentBuffer;
+    final Environment.Namespace nestedContentNamespace;
+    final Template nestedContentTemplate;
+    final List nestedContentParameterNames;
+    final LocalContextStack prevLocalContextStack;
+    final CallableInvocationContext prevMacroContext;
+    
+    CallableInvocationContext(UnboundCallable callableDefinition,
+            Environment env, 
+            TemplateElement[] nestedContentBuffer,
+            List nestedContentParameterNames) {
+        this.callableDefinition = callableDefinition;
+        this.localVars = env.new Namespace();
+        this.nestedContentBuffer = nestedContentBuffer;
+        this.nestedContentNamespace = env.getCurrentNamespace();
+        this.nestedContentTemplate = env.getCurrentTemplate();
+        this.nestedContentParameterNames = nestedContentParameterNames;
+        this.prevLocalContextStack = env.getLocalContextStack();
+        this.prevMacroContext = env.getCurrentMacroContext();
+    }
+    
+    Macro getCallableDefinition() {
+        return callableDefinition;
+    }
+
+    // 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 #", Integer.valueOf(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/CommandLine.java b/src/main/java/freemarker/core/CommandLine.java
deleted file mode 100644
index 95d0164..0000000
--- a/src/main/java/freemarker/core/CommandLine.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package freemarker.core;
-
-import freemarker.template.Configuration;
-import freemarker.template.Version;
-import freemarker.template.utility.DateUtil;
-
-/**
- * FreeMarker command-line utility, the Main-Class of <tt>freemarker.jar</tt>.
- * Currently it just prints the version number.
- * 
- * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
- */
-@Deprecated
-public class CommandLine {
-    
-    public static void main(String[] args) {
-        Version ver = Configuration.getVersion();
-        
-        System.out.println();
-        System.out.print("FreeMarker version ");
-        System.out.print(ver);
-        
-        /* If the version number doesn't already contain the build date and it's known, print it: */
-        if (!ver.toString().endsWith("Z")
-        		&& ver.getBuildDate() != null) {
-	        System.out.print(" (built on ");
-	        System.out.print(DateUtil.dateToISO8601String(
-	        		ver.getBuildDate(),
-	        		true, true, true, DateUtil.ACCURACY_SECONDS,
-	        		DateUtil.UTC,
-	        		new DateUtil.TrivialDateToISO8601CalendarFactory()));
-	        System.out.print(")");
-        }
-        System.out.println();
-        
-        if (ver.isGAECompliant() != null) {
-            System.out.print("Google App Engine complian variant: ");
-            System.out.println(ver.isGAECompliant().booleanValue() ? "Yes" : "No");
-        }
-        
-        System.out.println();
-        System.out.println("Copyright 2015 The Apache Software Foundation.");
-        System.out.println("Licensed under the Apache License, Version 2.0");
-        System.out.println();
-        System.out.println("For more information and for updates visit our Web site:");
-        System.out.println("http://freemarker.org/");
-        System.out.println();
-    }
-}
\ 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 5594ba0..5d2329c 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -31,7 +31,6 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -62,6 +61,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;
 
@@ -295,8 +295,12 @@
     };
 
     private Configurable parent;
+    
     private Properties properties;
-    private HashMap<Object, Object> 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;
@@ -401,8 +405,6 @@
 
         setBooleanFormat(C_TRUE_FALSE);
         
-        customAttributes = new HashMap();
-        
         customDateFormats = Collections.emptyMap();
         customNumberFormats = Collections.emptyMap();
     }
@@ -418,14 +420,14 @@
         classicCompatible = null;
         templateExceptionHandler = null;
         properties = new Properties(parent.properties);
-        customAttributes = new HashMap(0);
     }
     
     @Override
     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;
     }
     
@@ -2265,34 +2267,44 @@
     }
 
     /**
-     * Internal entry point for setting unnamed custom attributes.
+     * Used internally for setting custom attributes, both named and unnamed ones.
      * 
      * @see CustomAttribute
      */
     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.
      * 
      * @see CustomAttribute
      */
     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;
         }
     }
     
     boolean isCustomAttributeSet(Object key) {
-        return customAttributes.containsKey(key);
+        return getDirectCustomAttributes().containsKey(key);
     }
     
     /**
@@ -2304,8 +2316,8 @@
      * @since 2.3.24
      */
     void copyDirectCustomAttributes(Configurable target, boolean overwriteExisting) {
-        synchronized (customAttributes) {
-            for (Entry<? extends Object, ? extends Object> custAttrEnt : customAttributes.entrySet()) {
+        synchronized (customAttributesLock) {
+            for (Entry<? extends Object, ? extends Object> custAttrEnt : getDirectCustomAttributes().entrySet()) {
                 Object custAttrKey = custAttrEnt.getKey();
                 if (overwriteExisting || !target.isCustomAttributeSet(custAttrKey)) {
                     if (custAttrKey instanceof String) {
@@ -2319,6 +2331,28 @@
     }
     
     /**
+     * For internal usage only, returns the custom attributes set directly on this objects as a read-only {@link Map}.
+     * The returned {@link Map} won't necessarily reflect the changes made later, nor is it promised to be a snapshot. 
+     */
+    private Map<? extends Object, ? extends Object> getDirectCustomAttributes() {
+        synchronized (customAttributesLock) {
+            Map<? extends Object, ? extends Object> result = customAttributes != null ? customAttributes
+                    : getInitialCustomAttributes();
+            return result != null ? result : Collections.emptyMap();
+        }
+    }
+    
+    /**
+     * 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
@@ -2328,28 +2362,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;
         }
     }
     
@@ -2364,8 +2414,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);
+            }
         }
     }
 
@@ -2383,10 +2444,23 @@
      */
     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) {
@@ -2395,6 +2469,40 @@
         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 {
         if (parent != null) parent.doAutoImportsAndIncludes(env);
diff --git a/src/main/java/freemarker/core/DebugBreak.java b/src/main/java/freemarker/core/DebugBreak.java
index 2fa1cf3..32efcac 100644
--- a/src/main/java/freemarker/core/DebugBreak.java
+++ b/src/main/java/freemarker/core/DebugBreak.java
@@ -40,7 +40,7 @@
     @Override
     protected TemplateElement[] accept(Environment env) throws TemplateException, IOException {
         if (!DebuggerService.suspendEnvironment(
-                env, this.getTemplate().getSourceName(), getChild(0).getBeginLine())) {
+                env, this.getUnboundTemplate().getSourceName(), getChild(0).getBeginLine())) {
             return getChild(0).accept(env);
         } else {
             throw new StopException(env, "Stopped by debugger");
diff --git a/src/main/java/freemarker/core/DirectiveCallPlace.java b/src/main/java/freemarker/core/DirectiveCallPlace.java
index a16c311..3adaaef 100644
--- a/src/main/java/freemarker/core/DirectiveCallPlace.java
+++ b/src/main/java/freemarker/core/DirectiveCallPlace.java
@@ -36,12 +36,6 @@
  * directive call is first executed, via {@link #getOrCreateCustomData(Object, ObjectFactory)}.
  * 
  * <p>
- * Currently this method doesn't give you access to the {@link Template} object, because it's probable that future
- * versions of FreeMarker will be able to use the same parsed representation of a "file" for multiple {@link Template}
- * objects. Then the call place will be bound to the parsed representation, not to the {@link Template} objects that are
- * based on it.
- * 
- * <p>
  * <b>Don't implement this interface yourself</b>, as new methods can be added to it any time! It's only meant to be
  * implemented by the FreeMarker core.
  * 
@@ -56,6 +50,11 @@
 public interface DirectiveCallPlace {
 
     /**
+     * The template that directly contains the invocation of the directive.
+     */
+    UnboundTemplate getUnboundTemplate();
+    
+    /**
      * The 1-based column number of the first character of the directive call in the template source code, or -1 if it's
      * not known.
      */
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 3d61aaa..a0ff3af 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -157,11 +157,14 @@
 
     private Collator cachedCollator;
 
+    private Template currentTemplate;
+    private Namespace currentNamespace;
+    private CallableInvocationContext currentMacroContext;
+    
     private Writer out;
-    private Macro.Context currentMacroContext;
     private LocalContextStack localContextStack;
     private final Namespace mainNamespace;
-    private Namespace currentNamespace, globalNamespace;
+    private Namespace globalNamespace;
     private HashMap loadedLibs;
     private Configurable legacyParent;
 
@@ -169,7 +172,6 @@
     private Throwable lastThrowable;
 
     private TemplateModel lastReturnValue;
-    private HashMap macroToNamespaceLookup = new HashMap();
 
     private TemplateNodeModel currentVisitorNode;
     private TemplateSequenceModel nodeNamespaces;
@@ -201,9 +203,10 @@
         configuration = template.getConfiguration();
         this.globalNamespace = new Namespace(null);
         this.currentNamespace = mainNamespace = new Namespace(template);
+        this.currentTemplate = getMainTemplate();
         this.out = out;
         this.rootDataModel = rootDataModel;
-        importMacros(template);
+        predefineCallables(template);
     }
 
     /**
@@ -212,9 +215,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.
      */
     @Deprecated
     public Template getTemplate() {
@@ -252,8 +255,7 @@
      */
     @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm")
     public Template getCurrentTemplate() {
-        int ln = instructionStackSize;
-        return ln == 0 ? getMainTemplate() : instructionStack[ln - 1].getTemplate();
+        return currentTemplate;
     }
 
     /**
@@ -547,13 +549,18 @@
      * Used for {@code #nested}.
      */
     void invokeNestedContent(BodyInstruction.Context bodyCtx) throws TemplateException, IOException {
-        Macro.Context invokingMacroContext = getCurrentMacroContext();
+        CallableInvocationContext invokingMacroContext = getCurrentMacroContext();
         LocalContextStack prevLocalContextStack = localContextStack;
         TemplateElement[] nestedContentBuffer = invokingMacroContext.nestedContentBuffer;
         if (nestedContentBuffer != 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 = isBeforeIcI2322();
             prevParent = getParent();
@@ -574,7 +581,8 @@
                     localContextStack.pop();
                 }
                 this.currentMacroContext = invokingMacroContext;
-                currentNamespace = getMacroNamespace(invokingMacroContext.getMacro());
+                currentNamespace = prevCurrentNamespace;
+                currentTemplate = prevCurrentTemplate;
                 if (parentReplacementOn) {
                     setParent(prevParent);
                 } else {
@@ -622,8 +630,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);
             } else {
@@ -678,8 +686,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);
         }
@@ -688,30 +696,34 @@
     /**
      * 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[] childBuffer) 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, childBuffer, bodyParameterNames);
-            setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs);
+            final CallableInvocationContext macroCtx = new CallableInvocationContext(unboundCallable, this, childBuffer, bodyParameterNames);
+            setMacroContextLocalsFromArguments(macroCtx, unboundCallable, namedArgs, positionalArgs);
 
-            final Macro.Context prevMacroCtx = currentMacroContext;
+            final CallableInvocationContext prevMacroCtx = currentMacroContext;
             currentMacroContext = macroCtx;
 
             final LocalContextStack 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();
 
             try {
                 macroCtx.sanityCheck(this);
-                visit(macro.getChildBuffer());
+                visit(unboundCallable.getChildBuffer());
             } catch (ReturnInstruction.Return re) {
                 // Not an error, just a <#return>
             } catch (TemplateException te) {
@@ -719,7 +731,8 @@
             } finally {
                 currentMacroContext = prevMacroCtx;
                 localContextStack = prevLocalContextStack;
-                currentNamespace = prevNamespace;
+                currentNamespace = prevCurrentNamespace;
+                currentTemplate = prevCurrentTemplate;
             }
         } finally {
             popElement();
@@ -730,10 +743,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) {
@@ -746,7 +759,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);
@@ -757,7 +770,7 @@
                     }
                 } else {
                     throw new _MiscTemplateException(this,
-                            (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+                            (unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.getName()),
                             " has no parameter with name ", new _DelayedJQuote(argName), ".");
                 }
             }
@@ -770,11 +783,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,
-                        (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), ".");
             }
@@ -798,13 +811,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)
@@ -826,7 +836,7 @@
         }
     }
 
-    Macro.Context getCurrentMacroContext() {
+    CallableInvocationContext getCurrentMacroContext() {
         return currentMacroContext;
     }
 
@@ -2135,7 +2145,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("]");
     }
@@ -2363,7 +2373,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 {
@@ -2376,25 +2386,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;
                     }
                 }
@@ -2500,15 +2510,21 @@
             legacyParent = includedTemplate;
         }
 
-        importMacros(includedTemplate);
+        final Template prevCurrentTemplate = currentTemplate;
         try {
-            visit(includedTemplate.getRootTreeNode());
-        } finally {
-            if (parentReplacementOn) {
-                setParent(prevTemplate);
+            currentTemplate = includedTemplate;
+            predefineCallables(includedTemplate);
+            try {
+                visit(includedTemplate.getRootTreeNode());
+            } finally {
+                if (parentReplacementOn) {
+                    setParent(prevTemplate);
             } else {
                 legacyParent = prevTemplate;
+                }
             }
+        } finally {
+            currentTemplate = prevCurrentTemplate;
         }
     }
 
@@ -2633,9 +2649,16 @@
         }
     }
 
-    void importMacros(Template template) {
-        for (Iterator it = template.getMacros().values().iterator(); it.hasNext();) {
-            visitMacroDef((Macro) it.next());
+    /**
+     * Used for creating the callables that are defined by #macro/#function elements that weren't executed yet.  
+     */
+    void predefineCallables(Template template) {
+        final Map<String, UnboundCallable> unboundCallables
+                = _CoreAPI.getUnboundCallables(template.getUnboundTemplate());
+        if (unboundCallables != null) {
+            for (UnboundCallable unboundCallable : unboundCallables.values()) {
+                visitCallableDefinition(unboundCallable);
+            }
         }
     }
 
@@ -2754,6 +2777,12 @@
         public Template getTemplate() {
             return template == null ? Environment.this.getTemplate() : template;
         }
+        
+        @Override
+        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 886308c..c716a54 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -227,7 +227,7 @@
                     env != null
                         ? env.getArithmeticEngine()
                         : (leftExp != null
-                            ? leftExp.getTemplate().getArithmeticEngine()
+                            ? leftExp.getUnboundTemplate().getConfiguration().getArithmeticEngine()
                             : ArithmeticEngine.BIGDECIMAL_ENGINE);
             try {
                 cmpResult = ae.compareNumbers(leftNum, rightNum);
@@ -560,7 +560,7 @@
     static ArithmeticEngine getArithmeticEngine(Environment env, TemplateObject tObj) {
         return env != null
                 ? env.getArithmeticEngine()
-                : tObj.getTemplate().getParserConfiguration().getArithmeticEngine();
+                : tObj.getUnboundTemplate().getParserConfiguration().getArithmeticEngine();
     }
     
 }
diff --git a/src/main/java/freemarker/core/Expression.java b/src/main/java/freemarker/core/Expression.java
index 1b07c7d..fec7090 100644
--- a/src/main/java/freemarker/core/Expression.java
+++ b/src/main/java/freemarker/core/Expression.java
@@ -21,7 +21,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;
@@ -58,8 +57,8 @@
     // Hook in here to set the constant value if possible.
     
     @Override
-    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
-        super.setLocation(template, beginColumn, beginLine, endColumn, endLine);
+    void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int 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 42526f3..69b3484 100644
--- a/src/main/java/freemarker/core/IfBlock.java
+++ b/src/main/java/freemarker/core/IfBlock.java
@@ -58,7 +58,7 @@
         throws ParseException {
         if (getChildCount() == 1) {
             ConditionalBlock cblock = (ConditionalBlock) getChild(0);
-            cblock.setLocation(getTemplate(), cblock, this);
+            cblock.setLocation(getUnboundTemplate(), cblock, this);
             return cblock.postParseCleanup(stripWhitespace);
         } else {
             return super.postParseCleanup(stripWhitespace);
diff --git a/src/main/java/freemarker/core/Include.java b/src/main/java/freemarker/core/Include.java
index 08c8459..755a32f 100644
--- a/src/main/java/freemarker/core/Include.java
+++ b/src/main/java/freemarker/core/Include.java
@@ -41,12 +41,12 @@
     private final Boolean ignoreMissingExpPrecalcedValue;
 
     /**
-     * @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;
@@ -83,7 +83,7 @@
                         parse = Boolean.valueOf(StringUtil.getYesNo(parseExp.evalAndCoerceToPlainText(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 {
                     ignoreMissingExpPrecalcedValue = 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.evalAndCoerceToPlainText(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,
                     "Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
diff --git a/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java b/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
deleted file mode 100644
index 1f95dac..0000000
--- a/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package freemarker.core;
-
-import freemarker.template.Version;
-
-/**
- * Used to work around that {@link FMParser} has constructors that have separate parameters for individual settings.
- * 
- * @since 2.3.24
- */
-class LegacyConstructorParserConfiguration implements ParserConfiguration {
-
-    private final int tagSyntax;
-    private final int namingConvention;
-    private final boolean whitespaceStripping;
-    private final boolean strictSyntaxMode;
-    private ArithmeticEngine arithmeticEngine;
-    private Integer autoEscapingPolicy; 
-    private OutputFormat outputFormat;
-    private Boolean recognizeStandardFileExtensions; 
-    private final Version incompatibleImprovements;
-
-    public LegacyConstructorParserConfiguration(boolean strictSyntaxMode, boolean whitespaceStripping, int tagSyntax,
-            int namingConvention, Integer autoEscaping, OutputFormat outputFormat,
-            Boolean recognizeStandardFileExtensions,
-            Version incompatibleImprovements, ArithmeticEngine arithmeticEngine) {
-        this.tagSyntax = tagSyntax;
-        this.namingConvention = namingConvention;
-        this.whitespaceStripping = whitespaceStripping;
-        this.strictSyntaxMode = strictSyntaxMode;
-        this.autoEscapingPolicy = autoEscaping;
-        this.outputFormat = outputFormat;
-        this.recognizeStandardFileExtensions = recognizeStandardFileExtensions;
-        this.incompatibleImprovements = incompatibleImprovements;
-        this.arithmeticEngine = arithmeticEngine;
-    }
-
-    public int getTagSyntax() {
-        return tagSyntax;
-    }
-
-    public int getNamingConvention() {
-        return namingConvention;
-    }
-
-    public boolean getWhitespaceStripping() {
-        return whitespaceStripping;
-    }
-
-    public boolean getStrictSyntaxMode() {
-        return strictSyntaxMode;
-    }
-
-    public Version getIncompatibleImprovements() {
-        return incompatibleImprovements;
-    }
-
-    public ArithmeticEngine getArithmeticEngine() {
-        if (arithmeticEngine == null) {
-            throw new IllegalStateException();
-        }
-        return arithmeticEngine;
-    }
-
-    void setArithmeticEngineIfNotSet(ArithmeticEngine arithmeticEngine) {
-        if (this.arithmeticEngine == null) {
-            this.arithmeticEngine = arithmeticEngine;
-        }
-    }
-
-    public int getAutoEscapingPolicy() {
-        if (autoEscapingPolicy == null) {
-            throw new IllegalStateException();
-        }
-        return autoEscapingPolicy.intValue();
-    }
-    
-    void setAutoEscapingPolicyIfNotSet(int autoEscapingPolicy) {
-        if (this.autoEscapingPolicy == null) {
-            this.autoEscapingPolicy = Integer.valueOf(autoEscapingPolicy);
-        }
-    }
-
-    public OutputFormat getOutputFormat() {
-        if (outputFormat == null) {
-            throw new IllegalStateException();
-        }
-        return outputFormat;
-    }
-
-    void setOutputFormatIfNotSet(OutputFormat outputFormat) {
-        if (this.outputFormat == null) {
-            this.outputFormat = outputFormat;
-        }
-    }
-
-    public boolean getRecognizeStandardFileExtensions() {
-        if (recognizeStandardFileExtensions == null) {
-            throw new IllegalStateException();
-        }
-        return recognizeStandardFileExtensions.booleanValue();
-    }
-    
-    void setRecognizeStandardFileExtensionsIfNotSet(boolean recognizeStandardFileExtensions) {
-        if (this.recognizeStandardFileExtensions == null) {
-            this.recognizeStandardFileExtensions = Boolean.valueOf(recognizeStandardFileExtensions);
-        }
-    }
-
-}
diff --git a/src/main/java/freemarker/core/LibraryLoad.java b/src/main/java/freemarker/core/LibraryLoad.java
index 9305144..4597539 100644
--- a/src/main/java/freemarker/core/LibraryLoad.java
+++ b/src/main/java/freemarker/core/LibraryLoad.java
@@ -34,15 +34,15 @@
 @Deprecated
 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) {
         this.namespace = namespace;
@@ -54,7 +54,7 @@
         final String importedTemplateName = importedTemplateNameExp.evalAndCoerceToPlainText(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,
                     "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 162cad3..b7fa41f 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -19,309 +19,34 @@
 
 package freemarker.core;
 
-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 backward 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.
  */
 @Deprecated
-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,
-            TemplateElements.EMPTY);
+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,
-            TemplateElements children) {
-        this.name = name;
-        this.paramNames = (String[]) argumentNames.toArray(
-                new String[argumentNames.size()]);
-        this.paramDefaults = args;
-        
-        this.function = function;
-        this.catchAllParamName = catchAllParamName; 
-        
-        this.setChildren(children);
-    }
+    public abstract String getCatchAll();
 
-    public String getCatchAll() {
-        return catchAllParamName;
-    }
-    
-    public String[] getArgumentNames() {
-        return 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;
-    }
-
-    @Override
-    TemplateElement[] accept(Environment env) {
-        env.visitMacroDef(this);
-        return null;
-    }
-
-    @Override
-    protected String dump(boolean canonical) {
-        StringBuilder sb = new StringBuilder();
-        if (canonical) sb.append('<');
-        sb.append(getNodeTypeSymbol());
-        sb.append(' ');
-        sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
-        if (function) sb.append('(');
-        int argCnt = paramNames.length;
-        for (int i = 0; i < argCnt; i++) {
-            if (function) {
-                if (i != 0) {
-                    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 (function) {
-                if (argCnt != 0) {
-                    sb.append(", ");
-                }
-            } else {
-                sb.append(' ');
-            }
-            sb.append(catchAllParamName);
-            sb.append("...");
-        }
-        if (function) sb.append(')');
-        if (canonical) {
-            sb.append('>');
-            sb.append(getChildrenCanonicalForm());
-            sb.append("</").append(getNodeTypeSymbol()).append('>');
-        }
-        return sb.toString();
-    }
-    
-    @Override
-    String getNodeTypeSymbol() {
-        return function ? "#function" : "#macro";
-    }
-    
-    public boolean isFunction() {
-        return function;
-    }
-
-    class Context implements LocalContext {
-        final Environment.Namespace localVars; 
-        final TemplateElement[] nestedContentBuffer;
-        final Environment.Namespace nestedContentNamespace;
-        final List nestedContentParameterNames;
-        final LocalContextStack prevLocalContextStack;
-        final Context prevMacroContext;
-        
-        Context(Environment env, 
-                TemplateElement[] nestedContentBuffer,
-                List nestedContentParameterNames) {
-            this.localVars = env.new Namespace(); 
-            this.nestedContentBuffer = nestedContentBuffer;
-            this.nestedContentNamespace = env.getCurrentNamespace();
-            this.nestedContentParameterNames = nestedContentParameterNames;
-            this.prevLocalContextStack = env.getLocalContextStack();
-            this.prevMacroContext = env.getCurrentMacroContext();
-        }
-                
-        
-        Macro getMacro() {
-            return Macro.this;
-        }
-
-        // 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(
-                                            "When calling macro ", new _DelayedJQuote(name), 
-                                            ", required parameter ", new _DelayedJQuote(argName),
-                                            " (parameter #", Integer.valueOf(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;
-        }
-    }
-
-    @Override
-    int getParameterCount() {
-        return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
-    }
-
-    @Override
-    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 Integer.valueOf(function ? TYPE_FUNCTION : TYPE_MACRO);
-            } else {
-                throw new IndexOutOfBoundsException();
-            }
-        }
-    }
-
-    @Override
-    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
-    boolean isNestedBlockRepeater() {
-        // Because of recursive calls
-        return true;
-    }
+    public abstract boolean isFunction();
     
 }
diff --git a/src/main/java/freemarker/core/MessageUtil.java b/src/main/java/freemarker/core/MessageUtil.java
index 3aebacc..8b65dc8 100644
--- a/src/main/java/freemarker/core/MessageUtil.java
+++ b/src/main/java/freemarker/core/MessageUtil.java
@@ -21,7 +21,6 @@
 
 import java.util.ArrayList;
 
-import freemarker.template.Template;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
@@ -56,37 +55,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 1cb42de..664eb90 100644
--- a/src/main/java/freemarker/core/MethodCall.java
+++ b/src/main/java/freemarker/core/MethodCall.java
@@ -64,16 +64,17 @@
             : arguments.getValueList(env);
             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 c7842de..c719c9d 100644
--- a/src/main/java/freemarker/core/NewBI.java
+++ b/src/main/java/freemarker/core/NewBI.java
@@ -23,7 +23,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;
@@ -48,7 +47,7 @@
     @Override
     TemplateModel _eval(Environment env)
             throws TemplateException {
-        return new ConstructorFunction(target.evalAndCoerceToPlainText(env), env, target.getTemplate());
+        return new ConstructorFunction(target.evalAndCoerceToPlainText(env), env);
     }
 
     class ConstructorFunction implements TemplateMethodModelEx {
@@ -56,9 +55,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,
                         "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 ba37df1..0237714 100644
--- a/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
+++ b/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
@@ -32,7 +32,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 b7cacf8..7540d52 100644
--- a/src/main/java/freemarker/core/ParseException.java
+++ b/src/main/java/freemarker/core/ParseException.java
@@ -45,6 +45,13 @@
 public class ParseException extends IOException implements FMParserConstants {
 
     /**
+     * The version identifier for this Serializable class.
+     * Increment only if the <i>serialized</i> form of the
+     * class changes.
+     */
+    private static final long serialVersionUID = 1L;
+    
+    /**
      * This is the last token that has been consumed successfully.  If
      * this object has been created due to a parse error, the token
      * following this token will (therefore) be the first error token.
@@ -135,16 +142,19 @@
     }
 
     /**
+     * @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
      */
+    @Deprecated
     public ParseException(String description, Template template,
             int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
             Throwable cause) {
@@ -156,7 +166,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
      */
@@ -166,8 +176,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
      */
     @Deprecated
@@ -180,15 +190,19 @@
     }
 
     /**
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token)} instead.
      * @since 2.3.20
      */
+    @Deprecated
     public ParseException(String description, Template template, Token tk) {
         this(description, template, tk, null);
     }
 
     /**
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token, Throwable)} instead.
      * @since 2.3.20
      */
+    @Deprecated
     public ParseException(String description, Template template, Token tk, Throwable cause) {
         this(description,
                 template == null ? null : template.getSourceName(),
@@ -198,6 +212,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) {
@@ -209,7 +262,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 1cb81a3..e975f84 100644
--- a/src/main/java/freemarker/core/PropertySetting.java
+++ b/src/main/java/freemarker/core/PropertySetting.java
@@ -102,7 +102,7 @@
                     }
                 }
             }
-            throw new ParseException(sb.toString(), null, keyTk);
+            throw new ParseException(sb.toString(), (UnboundTemplate) null, keyTk);
         }
         
         this.key = key;
diff --git a/src/main/java/freemarker/core/StringLiteral.java b/src/main/java/freemarker/core/StringLiteral.java
index aa32d3e..91c429a 100644
--- a/src/main/java/freemarker/core/StringLiteral.java
+++ b/src/main/java/freemarker/core/StringLiteral.java
@@ -23,7 +23,6 @@
 import java.util.List;
 
 import freemarker.template.SimpleScalar;
-import freemarker.template.Template;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateScalarModel;
@@ -50,7 +49,7 @@
         // but we can't fix this backward compatibly.
         if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) {
             
-            Template parentTemplate = getTemplate();
+            UnboundTemplate parentTemplate = getUnboundTemplate();
 
             try {
                 FMParserTokenManager tkMan = new FMParserTokenManager(
@@ -59,7 +58,7 @@
                                 beginLine, beginColumn + 1,
                                 value.length()));
                 
-                FMParser parser = new FMParser(parentTemplate, false, tkMan, parentTemplate.getParserConfiguration());
+                FMParser parser = new FMParser(parentTemplate, false, tkMan, null, parentTemplate.getParserConfiguration());
                 // We continue from the parent parser's current state:
                 parser.setupStringLiteralMode(parentTkMan, outputFormat);
                 try {
diff --git a/src/main/java/freemarker/core/TemplateElementArrayBuilder.java b/src/main/java/freemarker/core/TemplateElementArrayBuilder.java
index 0126c19..f3b9a3d 100644
--- a/src/main/java/freemarker/core/TemplateElementArrayBuilder.java
+++ b/src/main/java/freemarker/core/TemplateElementArrayBuilder.java
@@ -80,7 +80,7 @@
             } else {
                 MixedContent mixedContent = new MixedContent();
                 mixedContent.setChildren(this);
-                mixedContent.setLocation(first.getTemplate(), first, getLast());
+                mixedContent.setLocation(first.getUnboundTemplate(), first, getLast());
                 return mixedContent;
             }
         }
@@ -94,7 +94,7 @@
         if (count != 0) {
             TemplateElement first = buffer[0];
             mixedContent.setChildren(this);
-            mixedContent.setLocation(first.getTemplate(), first, getLast());
+            mixedContent.setLocation(first.getUnboundTemplate(), first, getLast());
         }
         return mixedContent;
     }
diff --git a/src/main/java/freemarker/core/TemplateObject.java b/src/main/java/freemarker/core/TemplateObject.java
index f3f9e05..805af1c 100644
--- a/src/main/java/freemarker/core/TemplateObject.java
+++ b/src/main/java/freemarker/core/TemplateObject.java
@@ -34,7 +34,7 @@
 @Deprecated
 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,
@@ -42,89 +42,112 @@
      *  by a negative line numbers, starting from this constant as line 1. */
     static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000;  
 
-    final void setLocation(Template template, Token begin, Token end) {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    final void setLocation(UnboundTemplate unboundTemplate, Token begin, Token end) {
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
 
-    final void setLocation(Template template, Token tagBegin, Token tagEnd, TemplateElements children) {
+    final void setLocation(UnboundTemplate unboundTemplate, Token tagBegin, Token tagEnd, TemplateElements children) {
         TemplateElement lastChild = children.getLast();
         if (lastChild != null) {
             // [<#if exp>children]<#else>
-            setLocation(template, tagBegin, lastChild);
+            setLocation(unboundTemplate, tagBegin, lastChild);
         } else {
             // [<#if exp>]<#else>
-            setLocation(template, tagBegin, tagEnd);
+            setLocation(unboundTemplate, tagBegin, tagEnd);
         }
     }
     
-    final void setLocation(Template template, Token begin, TemplateObject end) {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    final void setLocation(UnboundTemplate unboundTemplate, Token begin, TemplateObject end) {
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
     
-    final void setLocation(Template template, TemplateObject begin, Token end) {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, Token end) {
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
 
-    final void setLocation(Template template, TemplateObject begin, TemplateObject end) {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+    final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, TemplateObject end) {
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
 
-    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
-        this.template = template;
+    void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine) {
+        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;
         }
@@ -145,8 +168,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) {
@@ -166,15 +191,16 @@
     }
 
     /**
-     * @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
      */
-    @Deprecated
-    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;
@@ -183,8 +209,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 26d54aa..51ce3fc 100644
--- a/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
+++ b/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
@@ -79,7 +79,7 @@
     static class ThreadInterruptionCheck extends TemplateElement {
         
         private ThreadInterruptionCheck(TemplateElement te) throws ParseException {
-            setLocation(te.getTemplate(), te.beginColumn, te.beginLine, te.beginColumn, te.beginLine);
+            setLocation(te.getUnboundTemplate(), te.beginColumn, te.beginLine, te.beginColumn, te.beginLine);
         }
 
         @Override
diff --git a/src/main/java/freemarker/core/TokenMgrError.java b/src/main/java/freemarker/core/TokenMgrError.java
index ae37ae6..5f46a14 100644
--- a/src/main/java/freemarker/core/TokenMgrError.java
+++ b/src/main/java/freemarker/core/TokenMgrError.java
@@ -21,6 +21,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}.
@@ -32,6 +34,14 @@
  * @see ParseException
  */
 public class TokenMgrError extends Error {
+    
+    /**
+     * The version identifier for this Serializable class.
+     * Increment only if the <i>serialized</i> form of the
+     * class changes.
+     */
+    private static final long serialVersionUID = 1L;
+    
    /*
     * Ordinals for various reasons why an Error of this type can be thrown.
     */
@@ -67,59 +77,57 @@
    private Integer endLineNumber, endColumnNumber;
 
    /**
-    * Replaces unprintable characters by their espaced (or unicode escaped)
+    * Replaces unprintable characters by their escaped (or unicode escaped)
     * equivalents in the given string
     */
    protected static final String addEscapes(String str) {
-      StringBuilder retval = new StringBuilder();
-      char ch;
-      for (int i = 0; i < str.length(); i++) {
-        switch (str.charAt(i))
-        {
-           case 0 :
-              continue;
-           case '\b':
-              retval.append("\\b");
-              continue;
-           case '\t':
-              retval.append("\\t");
-              continue;
-           case '\n':
-              retval.append("\\n");
-              continue;
-           case '\f':
-              retval.append("\\f");
-              continue;
-           case '\r':
-              retval.append("\\r");
-              continue;
-           case '\"':
-              retval.append("\\\"");
-              continue;
-           case '\'':
-              retval.append("\\\'");
-              continue;
-           case '\\':
-              retval.append("\\\\");
-              continue;
-           default:
-              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
-                 String s = "0000" + Integer.toString(ch, 16);
-                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
-              } else {
-                 retval.append(ch);
-              }
-              continue;
-        }
-      }
-      return retval.toString();
+     StringBuilder retval = new StringBuilder();
+     char ch;
+     for (int i = 0; i < str.length(); i++) {
+       switch (str.charAt(i))
+       {
+         case '\b':
+           retval.append("\\b");
+           continue;
+         case '\t':
+           retval.append("\\t");
+           continue;
+         case '\n':
+           retval.append("\\n");
+           continue;
+         case '\f':
+           retval.append("\\f");
+           continue;
+         case '\r':
+           retval.append("\\r");
+           continue;
+         case '\"':
+           retval.append("\\\"");
+           continue;
+         case '\'':
+           retval.append("\\\'");
+           continue;
+         case '\\':
+           retval.append("\\\\");
+           continue;
+         default:
+           if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+             String s = "0000" + Integer.toString(ch, 16);
+             retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+           } else {
+             retval.append(ch);
+           }
+           continue;
+       }
+     }
+     return retval.toString();
    }
 
    /**
-    * Returns a detailed message for the Error when it's thrown by the
+    * Returns a detailed message for the Error when it is thrown by the
     * token manager to indicate a lexical error.
-    * Parameters : 
-    *    EOFSeen     : indicates if EOF caused the lexicl error
+    * Parameters :
+    *    EOFSeen     : indicates if EOF caused the lexical error
     *    curLexState : lexical state in which this error occurred
     *    errorLine   : line number when the error occurred
     *    errorColumn : column number when the error occurred
@@ -127,10 +135,13 @@
     *    curchar     : the offending character
     * Note: You can customize the lexical error message by modifying this method.
     */
-   protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
-      return("Lexical error: encountered " +
-           (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int) curChar + "), ") +
-           "after \"" + addEscapes(errorAfter) + "\".");
+   protected static String LexicalErr(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar) {
+     char curChar1 = (char) curChar;
+     return("Lexical error at line " +
+           errorLine + ", column " +
+           errorColumn + ".  Encountered: " +
+           (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar1)) + "\"") + " (" + curChar + "), ") +
+           "after : \"" + addEscapes(errorAfter) + "\"");
    }
 
    /**
@@ -188,17 +199,8 @@
        this.endColumnNumber = Integer.valueOf(endColumnNumber); 
     }
 
-   /**
-    * Overload for JavaCC 6 compatibility.
-    * 
-    * @since 2.3.24
-    */
-   TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar, int reason) {
-       this(EOFSeen, lexState, errorLine, errorColumn, errorAfter, (char) curChar, reason);
-   }
-   
-   public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
-      this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
+   public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar, int reason) {
+      this(LexicalErr(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
       
       this.lineNumber = Integer.valueOf(errorLine);  // In J2SE there was no Integer.valueOf(int)
       this.columnNumber = Integer.valueOf(errorColumn);
@@ -251,9 +253,20 @@
        return detail;
    }
 
-   public ParseException toParseException(Template template) {
+   /**
+    * @deprecated Use {@link #toParseException(UnboundTemplate)} instead. 
+    */
+   @Deprecated
+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..4066818
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundCallable.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package 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 (or other future callable entity) 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,
+            TemplateElements.EMPTY);
+    
+    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,
+            TemplateElements children) {
+        this.name = name;
+        this.paramNames = (String[]) argumentNames.toArray(
+                new String[argumentNames.size()]);
+        this.paramDefaults = args;
+        
+        this.function = function;
+        this.catchAllParamName = catchAllParamName; 
+        
+        this.setChildren(children);
+    }
+    
+    String[] getParamNames() {
+        return paramNames;
+    }
+    
+    Map getParamDefaults() {
+        return paramDefaults;
+    }
+
+    @Override
+    public String getCatchAll() {
+        return catchAllParamName;
+    }
+    
+    @Override
+    public String[] getArgumentNames() {
+        return paramNames.clone();
+    }
+
+    String[] getArgumentNamesInternal() {
+        return paramNames;
+    }
+
+    boolean hasArgNamed(String name) {
+        return paramDefaults.containsKey(name);
+    }
+    
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    TemplateElement[] accept(Environment env) {
+        env.visitCallableDefinition(this);
+        return null;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
+        if (function) sb.append('(');
+        int argCnt = paramNames.length;
+        for (int i = 0; i < argCnt; i++) {
+            if (function) {
+                if (i != 0) {
+                    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 (function) {
+                if (argCnt != 0) {
+                    sb.append(", ");
+                }
+            } else {
+                sb.append(' ');
+            }
+            sb.append(catchAllParamName);
+            sb.append("...");
+        }
+        if (function) sb.append(')');
+        if (canonical) {
+            sb.append('>');
+            sb.append(getChildrenCanonicalForm());
+            sb.append("</").append(getNodeTypeSymbol()).append('>');
+        }
+        return sb.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return function ? "#function" : "#macro";
+    }
+    
+    @Override
+    boolean isShownInStackTrace() {
+        return false;
+    }
+    
+    @Override
+    boolean isNestedBlockRepeater() {
+        // Because of recursive calls
+        return true;
+    }
+    @Override
+    public boolean isFunction() {
+        return function;
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
+    }
+
+    @Override
+    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 Integer.valueOf(function ? TYPE_FUNCTION : TYPE_MACRO);
+            } else {
+                throw new IndexOutOfBoundsException();
+            }
+        }
+    }
+
+    @Override
+    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..7f7e96e
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundTemplate.java
@@ -0,0 +1,602 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package 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.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.UndeclaredThrowableException;
+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";
+
+    private final String sourceName;
+    private final Configuration cfg;
+    private final ParserConfiguration parserCfg;
+    private final Version templateLanguageVersion;
+    
+    /** Attributes added via {@code <#ftl attributes=...>}. */
+    private LinkedHashMap<String, Object> customAttributes;
+    private final Map<String, UnboundCallable> unboundCallables = new HashMap<String, UnboundCallable>(0);
+    // 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>(0));
+    private final TemplateElement rootElement;
+    private String defaultNamespaceURI;
+    private final int actualTagSyntax;
+    private final int actualNamingConvention;
+    private OutputFormat outputFormat;
+    private boolean autoEscaping;
+    
+    private final String templateSpecifiedEncoding;
+    
+    private final ArrayList lines = new ArrayList();
+    
+    private Map<String, String> prefixToNamespaceURIMapping;
+    private Map<String, String> namespaceURIToPrefixMapping;
+
+    /**
+     * @param reader
+     *            Reads the template source code
+     * @param cfg
+     *            The FreeMarker configuration settings; the resulting {@link UnboundTemplate} will be bound to this.
+     * @param customParserCfg
+     *            Overrides the parsing related configuration settings of the {@link Configuration} parameter; can be
+     *            {@code null}. See the similar paramter of
+     *            {@link Template#Template(String, String, Reader, Configuration, ParserConfiguration, String)} for more
+     *            details.
+     * @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, ParserConfiguration customParserCfg,
+            String assumedEncoding)
+            throws IOException {
+        NullArgumentException.check(cfg);
+        this.cfg = cfg;
+        this.parserCfg = customParserCfg != null ? customParserCfg : cfg;
+        this.sourceName = sourceName;
+        this.templateLanguageVersion = normalizeTemplateLanguageVersion(
+                getParserConfiguration().getIncompatibleImprovements());
+
+        LineTableBuilder ltbReader;
+        try {
+            if (!(reader instanceof BufferedReader) && !(reader instanceof StringReader)) {
+                reader = new BufferedReader(reader, 0x1000);
+            }
+            ltbReader = new LineTableBuilder(reader);
+            reader = ltbReader;
+
+            try {
+                FMParser parser = new FMParser(this, reader, assumedEncoding, getParserConfiguration());
+                
+                TemplateElement rootElement;
+                try {
+                    rootElement = parser.Root();
+                } catch (IndexOutOfBoundsException exc) {
+                    // There's a JavaCC bug where the Reader throws a RuntimeExcepton and then JavaCC fails with
+                    // IndexOutOfBoundsException. If that wasn't the case, we just rethrow. Otherwise we suppress the
+                    // IndexOutOfBoundsException and let the real cause to be thrown later. 
+                    if (!ltbReader.hasFailure()) {
+                        throw exc;
+                    }
+                    rootElement = null;
+                }
+                this.rootElement = rootElement;
+                
+                this.actualTagSyntax = parser._getLastTagSyntax();
+                this.actualNamingConvention = parser._getLastNamingConvention();
+                this.templateSpecifiedEncoding = parser._getTemplateSpecifiedEncoding();
+            } 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);
+            }
+        } catch (ParseException e) {
+            e.setTemplateName(getSourceName());
+            throw e;
+        } finally {
+            reader.close();
+        }
+        
+        // Throws any exception that JavaCC has silently treated as EOF:
+        ltbReader.throwFailure();
+
+        if (prefixToNamespaceURIMapping != null) {
+            prefixToNamespaceURIMapping = Collections.unmodifiableMap(prefixToNamespaceURIMapping);
+            namespaceURIToPrefixMapping = Collections.unmodifiableMap(namespaceURIToPrefixMapping);
+        }
+    }
+    
+    /**
+     * Creates a plain text (unparsed) template. 
+     */
+    static UnboundTemplate newPlainTextUnboundTemplate(String content, String sourceName, Configuration cfg) {
+        UnboundTemplate template;
+        try {
+            template = new UnboundTemplate(new StringReader("X"), sourceName, cfg, null, null);
+        } catch (IOException e) {
+            throw new BugException("Plain text template creation failed", e);
+        }
+        ((TextBlock) template.rootElement).replaceText(content);
+        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;
+        }
+    }
+    
+    /**
+     * Returns a string representing the raw template text in canonical form.
+     */
+    @Override
+    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;
+    }
+
+    /**
+     * See {@link Template#getActualTagSyntax()}.
+     */
+    public int getActualTagSyntax() {
+        return actualTagSyntax;
+    }
+    
+    /**
+     * See {@link Template#getActualNamingConvention()}.
+     */
+    public int getActualNamingConvention() {
+        return actualNamingConvention;
+    }
+    
+    /**
+     * See {@link Template#getOutputFormat()}.
+     */
+    public OutputFormat getOutputFormat() {
+        return outputFormat;
+    }
+    
+    /**
+     * Meant to be called by the parser only. 
+     */
+    void setOutputFormat(OutputFormat outputFormat) {
+        this.outputFormat = outputFormat;
+    }
+    
+    /**
+     * See {@link Template#getAutoEscaping()}.
+     */
+    public boolean getAutoEscaping() {
+        return autoEscaping;
+    }
+
+    /**
+     * Meant to be called by the parser only. 
+     */
+    void setAutoEscaping(boolean autoEscaping) {
+        this.autoEscaping = autoEscaping;
+    }
+    
+    public Configuration getConfiguration() {
+        return cfg;
+    }
+    
+    /**
+     * Returns the parser configuration that was in effect when creating this template; never {@code null}.
+     * See {@link Template#getParserConfiguration()} for details.
+     */
+    public ParserConfiguration getParserConfiguration() {
+        return parserCfg;
+    }
+
+    /**
+     * 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;
+        StringBuilder buf = new StringBuilder();
+        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 (prefixToNamespaceURIMapping != null) {
+            if (prefixToNamespaceURIMapping.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 {
+            if (prefixToNamespaceURIMapping == null) {
+                prefixToNamespaceURIMapping = new HashMap<String, String>();                
+                namespaceURIToPrefixMapping = new HashMap<String, String>();
+            }
+            prefixToNamespaceURIMapping.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;
+        }
+        
+        final Map<String, String> m = prefixToNamespaceURIMapping;
+        return m != null ? m.get(prefix) : null;
+    }
+    
+    /**
+     * The encoding (charset name) specified by the template itself (as of 2.3.22, via {@code <#ftl encoding=...>}), or
+     * {@code null} if none was specified.
+     */
+    public String getTemplateSpecifiedEncoding() {
+        return templateSpecifiedEncoding;
+    }
+
+    /**
+     * @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 "";
+        }
+        
+        final Map<String, String> m = namespaceURIToPrefixMapping;
+        return m != null ? m.get(nsURI) : null;
+    }
+
+    /**
+     * @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;
+    }
+
+    /**
+     * Reader that builds up the line table info for us, and also helps in working around JavaCC's exception
+     * suppression.
+     */
+    private class LineTableBuilder extends FilterReader {
+        
+        private final StringBuilder lineBuf = new StringBuilder();
+        int lastChar;
+        boolean closed;
+        
+        /** Needed to work around JavaCC behavior where it silently treats any errors as EOF. */ 
+        private Exception failure; 
+
+        /**
+         * @param r the character stream to wrap
+         */
+        LineTableBuilder(Reader r) {
+            super(r);
+        }
+        
+        public boolean hasFailure() {
+            return failure != null;
+        }
+
+        public void throwFailure() throws IOException {
+            if (failure != null) {
+                if (failure instanceof IOException) {
+                    throw (IOException) failure;
+                }
+                if (failure instanceof RuntimeException) {
+                    throw (RuntimeException) failure;
+                }
+                throw new UndeclaredThrowableException(failure);
+            }
+        }
+
+        @Override
+        public int read() throws IOException {
+            try {
+                int c = in.read();
+                handleChar(c);
+                return c;
+            } catch (Exception e) {
+                throw rememberException(e);
+            }
+        }
+
+        private IOException rememberException(Exception e) throws IOException {
+            // JavaCC used to read from the Reader after it was closed. So we must not treat that as a failure. 
+            if (!closed) {
+                failure = e;
+            }
+            if (e instanceof IOException) {
+                return (IOException) e;
+            }
+            if (e instanceof RuntimeException) {
+                throw (RuntimeException) e;
+            }
+            throw new UndeclaredThrowableException(e);
+        }
+
+        @Override
+        public int read(char cbuf[], int off, int len) throws IOException {
+            try {
+                int numchars = in.read(cbuf, off, len);
+                for (int i = off; i < off + numchars; i++) {
+                    char c = cbuf[i];
+                    handleChar(c);
+                }
+                return numchars;
+            } catch (Exception e) {
+                throw rememberException(e);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (lineBuf.length() > 0) {
+                lines.add(lineBuf.toString());
+                lineBuf.setLength(0);
+            }
+            super.close();
+            closed = true;
+        }
+
+        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 6608b0a..04a6d3a 100644
--- a/src/main/java/freemarker/core/UnifiedCall.java
+++ b/src/main/java/freemarker/core/UnifiedCall.java
@@ -70,17 +70,18 @@
 
     @Override
     TemplateElement[] accept(Environment env) throws TemplateException, IOException {
-        TemplateModel tm = nameExp.eval(env);
-        if (tm == Macro.DO_NOTHING_MACRO) return null; // 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 null; // shortcut here.
+        if (tm instanceof BoundCallable) {
+            final BoundCallable boundMacro = (BoundCallable) tm;
+            final Macro unboundMacro = boundMacro.getUnboundCallable();
+            if (unboundMacro.isFunction() && !legacySyntax) {
                 throw new _MiscTemplateException(env,
-                        "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, getChildBuffer());
+            env.invoke(boundMacro, namedArgs, positionalArgs, bodyParameterNames, getChildBuffer());
         } else {
             boolean isDirectiveModel = tm instanceof TemplateDirectiveModel; 
             if (isDirectiveModel || tm instanceof TemplateTransformModel) {
@@ -328,6 +329,10 @@
         
     }
     
+    public String getTemplateSourceName() {
+        return getUnboundTemplate().getSourceName();
+    }
+
     @Override
     boolean isNestedBlockRepeater() {
         return true;
diff --git a/src/test/java/freemarker/core/LegacyFMParserConstructorsTest.java b/src/main/java/freemarker/core/_2_4_OrLaterMarker.java
similarity index 67%
rename from src/test/java/freemarker/core/LegacyFMParserConstructorsTest.java
rename to src/main/java/freemarker/core/_2_4_OrLaterMarker.java
index 738d227..81736a1 100644
--- a/src/test/java/freemarker/core/LegacyFMParserConstructorsTest.java
+++ b/src/main/java/freemarker/core/_2_4_OrLaterMarker.java
@@ -16,22 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 package freemarker.core;
 
-import org.junit.Test;
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * Used internally for detecting duplicate FreeMarker jar-s with different versions.
+ */
+public class _2_4_OrLaterMarker {
 
-public class LegacyFMParserConstructorsTest {
-
-    @Test
-    public void test1() throws ParseException {
-        FMParser parser = new FMParser("x");
-        parser.Root();
-    }
-    
-    @Test
-    public void testCreateExpressionParser() throws ParseException {
-         FMParser parser = FMParser.createExpressionParser("x + y");
-         parser.Expression();
-    }
-    
 }
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index df95196..d5b3977 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -19,9 +19,15 @@
 
 package freemarker.core;
 
+import java.io.IOException;
+import java.io.Reader;
 import java.io.Writer;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -156,6 +162,80 @@
         Environment.outputInstructionStack(instructionStackSnapshot, terseMode, pw);
     }
 
+    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) {
+        final UnboundCallable unboundCallable = macroToUnboundCallable(macro);
+        unboundTemplate.addUnboundCallable(unboundCallable);
+    }
+
+    /**
+     * In 2.4 {@link Macro} was split to {@link BoundCallable} and {@link UnboundCallable}, but because of BC
+     * constraints sometimes we can only expect a {@link Macro}, but that can always converted to
+     * {@link UnboundCallable}.
+     */
+    private static UnboundCallable macroToUnboundCallable(Macro macro) {
+        if (macro instanceof UnboundCallable) {
+            // It's coming from the AST:
+            return (UnboundCallable) macro;
+        } else if (macro instanceof BoundCallable) {
+            // It's coming from an FTL variable:
+            return ((BoundCallable) macro).getUnboundCallable(); 
+        } else if (macro == null) {
+            return null;
+        } else {
+            // Impossible, Macro should have only two subclasses.
+            throw new BugException();
+        }
+    }
+
+    public static void addImport(UnboundTemplate unboundTemplate, LibraryLoad libLoad) {
+        unboundTemplate.addImport(libLoad);
+    }
+    
+    public static UnboundTemplate newUnboundTemplate(Reader reader, String sourceName,
+            Configuration cfg, ParserConfiguration parserCfg, String assumedEncoding) throws IOException {
+        return new UnboundTemplate(reader, sourceName, cfg, parserCfg, assumedEncoding);
+    }
+    
+    public static boolean isBoundCallable(Object obj) {
+        return obj instanceof BoundCallable;
+    }
+
+    public static UnboundTemplate newPlainTextUnboundTemplate(String content, String sourceName, Configuration config) {
+        return UnboundTemplate.newPlainTextUnboundTemplate(content, sourceName, 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<String, UnboundCallable> getUnboundCallables(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! 
@@ -172,9 +252,221 @@
             throws NestedContentNotSupportedException {
         NestedContentNotSupportedException.check(body);
     }
+
+    public static Map<String, Macro> createAdapterMacroMapForUnboundCallables(UnboundTemplate unboundTemplate) {
+        return new AdapterMacroMap(getUnboundCallables(unboundTemplate));
+    }
     
-    static final public void replaceText(TextBlock textBlock, String text) {
-        textBlock.replaceText(text);
+    /**
+     * Wraps a {@code Map<String, UnboundCallable>} as if it was a {@code Map<String, Macro>}. This is for backward
+     * compatibility. The important use case is being able to put any {@link Macro} subclass into this {@code Map},
+     * despite that the backing {@link Macro} can only store {@code UnboundCallable}. Reading works a bit strangely,
+     * because if you put a non-{@link UnboundCallable} value in, then get it with the same key, you get the
+     * corresponding {@link UnboundCallable} back instead of the original object. That's also a {@code Macro} though. 
+     */
+    private static class AdapterMacroMap implements Map<String, Macro> {
+        
+        private final Map<String, UnboundCallable> adapted;
+
+        public AdapterMacroMap(Map<String, UnboundCallable> unboundCallableMap) {
+            this.adapted = unboundCallableMap;
+        }
+
+        public int size() {
+            return adapted.size();
+        }
+
+        public boolean isEmpty() {
+            return adapted.isEmpty();
+        }
+
+        public boolean containsKey(Object key) {
+            return adapted.containsKey(key);
+        }
+
+        public boolean containsValue(Object value) {
+            return adapted.containsValue(value);
+        }
+
+        public UnboundCallable get(Object key) {
+            return adapted.get(key);
+        }
+
+        public UnboundCallable put(String key, Macro value) {
+            return adapted.put(key, macroToUnboundCallable(value));
+        }
+
+        public UnboundCallable remove(Object key) {
+            return adapted.remove(key);
+        }
+
+        public void putAll(Map<? extends String, ? extends Macro> t) {
+            for (Map.Entry<? extends String, ? extends Macro> ent : t.entrySet()) {
+                put(ent.getKey(), ent.getValue());
+            }
+        }
+
+        public void clear() {
+            adapted.clear();
+        }
+
+        public Set<String> keySet() {
+            return adapted.keySet();
+        }
+
+        public Collection<Macro> values() {
+            // According the Map API, this Collection doesn't allows adding elements, it's safe to treat as a
+            // Collection<Macro>.
+            return (Collection) adapted.values();
+        }
+
+        public Set<Entry<String, Macro>> entrySet() {
+            // According the Map API, this set doesn't allows adding elements, but, the Map.Entry-s are still
+            // modifiable.
+            return new AdapterMacroMapEntrySet(adapted.entrySet());
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return adapted.equals(o);
+        }
+
+        @Override
+        public int hashCode() {
+            return adapted.hashCode();
+        }
+        
+    }
+    
+    /** Helper for {@link AdapterMacroMap}. */
+    private static class AdapterMacroMapEntrySet implements Set<Map.Entry<String, Macro>> {
+        
+        private final Set<Map.Entry<String, UnboundCallable>> adapted;
+
+        public AdapterMacroMapEntrySet(Set<Entry<String, UnboundCallable>> adapted) {
+            this.adapted = adapted;
+        }
+
+        public int size() {
+            return adapted.size();
+        }
+
+        public boolean isEmpty() {
+            return adapted.isEmpty();
+        }
+
+        public boolean contains(Object o) {
+            return adapted.contains(o);
+        }
+
+        public Iterator<Entry<String, Macro>> iterator() {
+            return new AdapterMacroMapEntrySetIterator(adapted.iterator());
+        }
+
+        public Object[] toArray() {
+            return adapted.toArray();
+        }
+
+        public <T> T[] toArray(T[] a) {
+            return adapted.toArray(a);
+        }
+
+        public boolean add(Entry<String, Macro> o) {
+            // Won't be allowed anyway
+            return adapted.add((Entry) o);
+        }
+
+        public boolean remove(Object o) {
+            return adapted.remove(o);
+        }
+
+        public boolean containsAll(Collection<?> c) {
+            return adapted.containsAll(c);
+        }
+
+        public boolean addAll(Collection<? extends Entry<String, Macro>> c) {
+            // Won't be allowed anyway
+            return adapted.addAll((Collection) c);
+        }
+
+        public boolean retainAll(Collection<?> c) {
+            return adapted.retainAll(c);
+        }
+
+        public boolean removeAll(Collection<?> c) {
+            return adapted.removeAll(c);
+        }
+
+        public void clear() {
+            adapted.clear();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return adapted.equals(o);
+        }
+
+        @Override
+        public int hashCode() {
+            return adapted.hashCode();
+        }
+        
+    }
+    
+    /** Helper for {@link AdapterMacroMap}. */
+    private static class AdapterMacroMapEntrySetIterator implements Iterator<Map.Entry<String, Macro>> {
+        
+        private final Iterator<Map.Entry<String, UnboundCallable>> adapted;
+
+        public AdapterMacroMapEntrySetIterator(Iterator<Entry<String, UnboundCallable>> adapted) {
+            this.adapted = adapted;
+        }
+
+        public boolean hasNext() {
+            return adapted.hasNext();
+        }
+
+        public Entry<String, Macro> next() {
+            return new AdapterMacroMapEntry(adapted.next());
+        }
+
+        public void remove() {
+            adapted.remove();
+        }
+        
+    }
+    
+    /** Helper for {@link AdapterMacroMap}. */
+    private static class AdapterMacroMapEntry implements Map.Entry<String, Macro> {
+        
+        private final Map.Entry<String, UnboundCallable> adapted;
+
+        public AdapterMacroMapEntry(Entry<String, UnboundCallable> adapted) {
+            this.adapted = adapted;
+        }
+
+        public String getKey() {
+            return adapted.getKey();
+        }
+
+        public UnboundCallable getValue() {
+            return adapted.getValue();
+        }
+
+        public UnboundCallable setValue(Macro value) {
+            return adapted.setValue(macroToUnboundCallable(value));
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return adapted.equals(o);
+        }
+
+        @Override
+        public int hashCode() {
+            return adapted.hashCode();
+        }
+        
     }
 
     /**
diff --git a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
index a4b4731..b478713 100644
--- a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
+++ b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
@@ -208,7 +208,9 @@
     }
 
     private void appendParts(StringBuilder 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[]) {
@@ -220,7 +222,7 @@
                     partStr = "null";
                 }
                 
-                if (template != null) {
+                if (unboundTemplate != null) {
                     if (partStr.length() > 4
                             && partStr.charAt(0) == '<'
                             && (
@@ -228,7 +230,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/ext/dom/Transform.java b/src/main/java/freemarker/ext/dom/Transform.java
deleted file mode 100644
index 5df9683..0000000
--- a/src/main/java/freemarker/ext/dom/Transform.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package freemarker.ext.dom;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.util.Locale;
-import java.util.StringTokenizer;
-
-import freemarker.core.Environment;
-import freemarker.template.Configuration;
-import freemarker.template.Template;
-
-/**
- * A class that contains a main() method for command-line invocation of a FreeMarker XML transformation.
- * 
- * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
- */
-@Deprecated
-public class Transform {
-
-    private File inputFile, ftlFile, outputFile;
-    private String encoding;
-    private Locale locale;
-    private Configuration cfg;
-
-    /**
-     * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
-     */
-    @Deprecated
-    static public void main(String[] args) {
-        try {
-            Transform proc = transformFromArgs(args);
-            proc.transform();
-        } catch (IllegalArgumentException iae) {
-            System.err.println(iae.getMessage());
-            usage();
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    /**
-     * @param inputFile
-     *            The file from which to read the XML input
-     * @param ftlFile
-     *            The file containing the template
-     * @param outputFile
-     *            The file to which to output. If this is null, we use stdout.
-     * @param locale
-     *            The locale to use. If this is null, we use the platform default.
-     * @param encoding
-     *            The character encoding to use for output, if this is null, we use the platform default
-     */
-    Transform(File inputFile, File ftlFile, File outputFile, Locale locale, String encoding) throws IOException {
-        if (encoding == null) {
-            encoding = System.getProperty("file.encoding");
-        }
-        if (locale == null) {
-            locale = Locale.getDefault();
-        }
-        this.encoding = encoding;
-        this.locale = locale;
-        this.inputFile = inputFile;
-        this.ftlFile = ftlFile;
-        this.outputFile = outputFile;
-        File ftlDirectory = ftlFile.getAbsoluteFile().getParentFile();
-        cfg = new Configuration();
-        cfg.setDirectoryForTemplateLoading(ftlDirectory);
-    }
-
-    /**
-     * Performs the transformation.
-     */
-    void transform() throws Exception {
-        String templateName = ftlFile.getName();
-        Template template = cfg.getTemplate(templateName, locale);
-        NodeModel rootNode = NodeModel.parse(inputFile);
-        OutputStream outputStream = System.out;
-        if (outputFile != null) {
-            outputStream = new FileOutputStream(outputFile);
-        }
-        Writer outputWriter = new OutputStreamWriter(outputStream, encoding);
-        try {
-            template.process(null, outputWriter, null, rootNode);
-        } finally {
-            if (outputFile != null)
-                outputWriter.close();
-        }
-    }
-
-    static Transform transformFromArgs(String[] args) throws IOException {
-        int i = 0;
-        String input = null, output = null, ftl = null, loc = null, enc = null;
-        while (i < args.length) {
-            String dashArg = args[i++];
-            if (i >= args.length) {
-                throw new IllegalArgumentException("");
-            }
-            String arg = args[i++];
-            if (dashArg.equals("-in")) {
-                if (input != null) {
-                    throw new IllegalArgumentException("The input file should only be specified once");
-                }
-                input = arg;
-            } else if (dashArg.equals("-ftl")) {
-                if (ftl != null) {
-                    throw new IllegalArgumentException("The ftl file should only be specified once");
-                }
-                ftl = arg;
-            } else if (dashArg.equals("-out")) {
-                if (output != null) {
-                    throw new IllegalArgumentException("The output file should only be specified once");
-                }
-                output = arg;
-            } else if (dashArg.equals("-locale")) {
-                if (loc != null) {
-                    throw new IllegalArgumentException("The locale should only be specified once");
-                }
-                loc = arg;
-            } else if (dashArg.equals("-encoding")) {
-                if (enc != null) {
-                    throw new IllegalArgumentException("The encoding should only be specified once");
-                }
-                enc = arg;
-            } else {
-                throw new IllegalArgumentException("Unknown input argument: " + dashArg);
-            }
-        }
-        if (input == null) {
-            throw new IllegalArgumentException("No input file specified.");
-        }
-        if (ftl == null) {
-            throw new IllegalArgumentException("No ftl file specified.");
-        }
-        File inputFile = new File(input).getAbsoluteFile();
-        File ftlFile = new File(ftl).getAbsoluteFile();
-        if (!inputFile.exists()) {
-            throw new IllegalArgumentException("Input file does not exist: " + input);
-        }
-        if (!ftlFile.exists()) {
-            throw new IllegalArgumentException("FTL file does not exist: " + ftl);
-        }
-        if (!inputFile.isFile() || !inputFile.canRead()) {
-            throw new IllegalArgumentException("Input file must be a readable file: " + input);
-        }
-        if (!ftlFile.isFile() || !ftlFile.canRead()) {
-            throw new IllegalArgumentException("FTL file must be a readable file: " + ftl);
-        }
-        File outputFile = null;
-        if (output != null) {
-            outputFile = new File(output).getAbsoluteFile();
-            File outputDirectory = outputFile.getParentFile();
-            if (!outputDirectory.exists() || !outputDirectory.canWrite()) {
-                throw new IllegalArgumentException("The output directory must exist and be writable: "
-                        + outputDirectory);
-            }
-        }
-        Locale locale = Locale.getDefault();
-        if (loc != null) {
-            locale = localeFromString(loc);
-        }
-        return new Transform(inputFile, ftlFile, outputFile, locale, enc);
-    }
-
-    static Locale localeFromString(String ls) {
-        if (ls == null) ls = "";
-        String lang = "", country = "", variant = "";
-        StringTokenizer st = new StringTokenizer(ls, "_-,");
-        if (st.hasMoreTokens()) {
-            lang = st.nextToken();
-            if (st.hasMoreTokens()) {
-                country = st.nextToken();
-                if (st.hasMoreTokens()) {
-                    variant = st.nextToken();
-                }
-            }
-            return new Locale(lang, country, variant);
-        } else {
-            return Locale.getDefault();
-        }
-    }
-
-    static void usage() {
-        System.err
-                .println("Usage: java freemarker.ext.dom.Transform -in <xmlfile> -ftl <ftlfile> [-out <outfile>] [-locale <locale>] [-encoding <encoding>]");
-        // Security: prevents shutting down the container from a template:
-        if (Environment.getCurrentEnvironment() == null) {
-            System.exit(-1);
-        }
-    }
-}
diff --git a/src/main/java/freemarker/ext/jdom/NodeListModel.java b/src/main/java/freemarker/ext/jdom/NodeListModel.java
index 077a7e0..aa187e0 100644
--- a/src/main/java/freemarker/ext/jdom/NodeListModel.java
+++ b/src/main/java/freemarker/ext/jdom/NodeListModel.java
@@ -19,7 +19,6 @@
 
 package freemarker.ext.jdom;
 
-import java.io.FileReader;
 import java.io.IOException;
 import java.io.Writer;
 import java.util.ArrayList;
@@ -49,9 +48,7 @@
 import org.jdom.Text;
 import org.jdom.output.XMLOutputter;
 
-import freemarker.template.SimpleHash;
 import freemarker.template.SimpleScalar;
-import freemarker.template.Template;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateHashModel;
 import freemarker.template.TemplateMethodModel;
@@ -1125,29 +1122,6 @@
         }
     }
 
-    /**
-     * Loads a template from a file passed as the first argument, loads an XML
-     * document from the standard input, passes it to the template as variable
-     * <tt>document</tt> and writes the result of template processing to
-     * standard output.
-     * 
-     * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
-     */
-    @Deprecated
-    public static void main(String[] args)
-    throws Exception {
-        org.jdom.input.SAXBuilder builder = new org.jdom.input.SAXBuilder();
-        Document document = builder.build(System.in);
-        SimpleHash model = new SimpleHash();
-        model.put("document", new NodeListModel(document));
-        FileReader fr = new FileReader(args[0]);
-        Template template = new Template(args[0], fr);
-        Writer w = new java.io.OutputStreamWriter(System.out);
-        template.process(model, w);
-        w.flush();
-        w.close();
-    }
-
     private static final class AttributeXMLOutputter extends XMLOutputter {
         public void output(Attribute attribute, Writer out)
         throws IOException {
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 82c46b3..5cf17df 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -390,6 +390,9 @@
 
     /** FreeMarker version 2.3.23 (an {@link #Configuration(Version) incompatible improvements break-point}) */
     public static final Version VERSION_2_3_23 = new Version(2, 3, 23);
+    
+    /** FreeMarker version 2.4.0 (an {@link #Configuration(Version) incompatible improvements break-point}) */
+    public static final Version VERSION_2_4_0 = new Version(2, 4, 0);
 
     /** FreeMarker version 2.3.24 (an {@link #Configuration(Version) incompatible improvements break-point}) */
     public static final Version VERSION_2_3_24 = new Version(2, 3, 24);
@@ -445,22 +448,22 @@
         }
     }
     
-    private static final String FM_24_DETECTION_CLASS_NAME = "freemarker.core._2_4_OrLaterMarker";
-    private static final boolean FM_24_DETECTED;
+    private static final String FM_23_DETECTION_CLASS_NAME = "freemarker.core.CommandLine";
+    private static final boolean FM_23_DETECTED;
     static {
-        boolean fm24detected;
+        boolean fm23detected;
         try {
-            Class.forName(FM_24_DETECTION_CLASS_NAME);
-            fm24detected = true;
+            Class.forName(FM_23_DETECTION_CLASS_NAME);
+            fm23detected = true;
         } catch (ClassNotFoundException e) {
-            fm24detected = false;
+            fm23detected = false;
         } catch (LinkageError e) {
-            fm24detected = true;
+            fm23detected = true;
         } catch (Throwable e) {
             // Unexpected. We assume that there's no clash.
-            fm24detected = false;
+            fm23detected = false;
         }
-        FM_24_DETECTED = fm24detected;
+        FM_23_DETECTED = fm23detected;
     }
     
     private final static Object defaultConfigLock = new Object();
@@ -811,9 +814,9 @@
     }
 
     private static void checkFreeMarkerVersionClash() {
-        if (FM_24_DETECTED) {
-            throw new RuntimeException("Clashing FreeMarker versions (" + VERSION + " and some post-2.3.x) detected: "
-                    + "found post-2.3.x class " + FM_24_DETECTION_CLASS_NAME + ". You probably have two different "
+        if (FM_23_DETECTED) {
+            throw new RuntimeException("Clashing FreeMarker versions (" + VERSION + " and some pre-2.4) detected: "
+                    + "found pre-2.4 class " + FM_23_DETECTION_CLASS_NAME + ". You probably have two different "
                     + "freemarker.jar-s in the classpath.");
         }
     }
@@ -2502,10 +2505,10 @@
     }
 
     /**
-     * Sets the character set encoding to use for templates of
-     * a given locale. If there is no explicit encoding set for some
-     * locale, then the default encoding will be used, what you can
-     * set with {@link #setDefaultEncoding}.
+     * Sets the charset (encoding) to use for parsing templates that are requested for a given locale. If there is no
+     * explicit encoding set for some locale, then the default encoding will be used, which you can set with
+     * {@link #setDefaultEncoding}. By default there are no encodings set for any locale, so always
+     * {@link #setDefaultEncoding} will be used.
      *
      * @see #clearEncodingMap
      * @see #loadBuiltInEncodingMap
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index f785768..91ee466 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -19,30 +19,21 @@
 
 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.lang.reflect.UndeclaredThrowableException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
-import java.util.Vector;
 
 import freemarker.cache.TemplateCache;
 import freemarker.cache.TemplateLoader;
 import freemarker.cache.TemplateLookupStrategy;
-import freemarker.core.BugException;
 import freemarker.core.Configurable;
 import freemarker.core.Environment;
-import freemarker.core.FMParser;
 import freemarker.core.LibraryLoad;
 import freemarker.core.Macro;
 import freemarker.core.OutputFormat;
@@ -50,68 +41,56 @@
 import freemarker.core.ParserConfiguration;
 import freemarker.core.TemplateConfiguration;
 import freemarker.core.TemplateElement;
-import freemarker.core.TextBlock;
-import freemarker.core.TokenMgrError;
+import freemarker.core.UnboundTemplate;
 import freemarker.core._CoreAPI;
 import freemarker.debug.impl.DebuggerService;
 
 /**
- * <p>
- * Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from multiple
- * threads.
+ * <p>Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from
+ * multiple threads.
  * 
- * <p>
- * Typically, you will use {@link Configuration#getTemplate(String)} to create/get {@link Template} objects, so you
- * don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String} that
- * contains the template source code. But then it's important to know that while the resulting {@link Template} is
- * efficient for later processing, creating a new {@link Template} itself is relatively expensive. So try to re-use
- * {@link Template} objects if possible. {@link Configuration#getTemplate(String)} (and its overloads) does that
- * (caching {@link Template}-s) for you, but the constructor of course doesn't, so it's up to you to solve then.
+ * <p>Typically, you will use {@link Configuration#getTemplate(String)} to create/get {@link Template} objects, so
+ * you don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String}
+ * that contains the template source code. But then it's
+ * important to know that while the resulting {@link Template} is efficient for later processing, creating a new
+ * {@link Template} itself is relatively expensive. So try to re-use {@link Template} objects if possible.
+ * {@link Configuration#getTemplate(String)} does that (caching {@link Template}-s) for you, but the constructor of
+ * course doesn't, so it's up to you to solve then.
  * 
- * <p>
- * Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods for
- * changing FreeMarker settings. Those must not be used while the template is being processed, or if the template object
- * is already accessible from multiple threads. If some templates need different settings that those coming from the
- * shared {@link Configuration}, and you are using {@link Configuration#getTemplate(String)} (or its overloads), then
- * see {@link Configuration#setTemplateConfigurations(freemarker.cache.TemplateConfigurationFactory)}.
+ * <p>Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods
+ * for changing FreeMarker settings. Those must not be used while the template is being processed, or if the
+ * 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 int actualNamingConvention;
-    private boolean autoEscaping;
-    private OutputFormat outputFormat;
+    private final UnboundTemplate unboundTemplate;
     private final String name;
-    private final String sourceName;
-    private final ArrayList lines = new ArrayList();
-    private final ParserConfiguration parserConfiguration;
-    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, ParserConfiguration customParserConfiguration) {
+    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());
-        this.parserConfiguration = customParserConfiguration != null ? customParserConfiguration : getConfiguration();
     }
 
+    /**
+     * To be used internally only!
+     */
+    Template(UnboundTemplate unboundTemplate,
+            String name, Locale locale, Object customLookupCondition,
+            Configuration cfg) {
+        this(unboundTemplate, name, cfg);
+        this.setLocale(locale);
+        this.setCustomLookupCondition(customLookupCondition);
+    }
+    
     private static Configuration toNonNull(Configuration cfg) {
         return cfg != null ? cfg : Configuration.getDefaultConfiguration();
     }
@@ -199,88 +178,51 @@
      * 
      * @since 2.3.22
      */
-   @Deprecated
-   public Template(
-           String name, String sourceName, Reader reader, Configuration cfg, String encoding) throws IOException {
-       this(name, sourceName, reader, cfg, null, encoding);
-   }
-   
+    @Deprecated
+    public Template(
+            String name, String sourceName, Reader reader, Configuration cfg, String encoding) throws IOException {
+        this(name, sourceName, reader, cfg, null, encoding);
+    }
+
     /**
      * Same as {@link #Template(String, String, Reader, Configuration, String)}, but also specifies a
      * {@link TemplateConfiguration}. This is mostly meant to be used by FreeMarker internally, but advanced users might
      * still find this useful.
      * 
-     * @param customParserConfiguration
+     * @param customParserCfg
      *            Overrides the parsing related configuration settings of the {@link Configuration} parameter; can be
      *            {@code null}. This is useful as the {@link Configuration} is normally a singleton shared by all
-     *            templates, and so it's not good for specifying template-specific settings. (While {@link Template}
-     *            itself has methods to specify settings just for that template, those don't influence the parsing, and
-     *            you only have opportunity to call them after the parsing anyway.) This objects is often a
-     *            {@link TemplateConfiguration} whose parent is the {@link Configuration} parameter, and then it
+     *            templates, and so it's not good for specifying template-specific settings. (While
+     *            {@link Template} itself has methods to specify settings just for that template, those don't influence
+     *            the parsing, and you only have opportunity to call them after the parsing anyway.) This objects is
+     *            often a {@link TemplateConfiguration} whose parent is the {@link Configuration} parameter, and then it
      *            practically just overrides some of the parser settings, as the others are inherited from the
-     *            {@link Configuration}. Note that if this is a {@link TemplateConfiguration}, you will also want to
-     *            call {@link TemplateConfiguration#apply(Template)} on the resulting {@link Template} so that
+     *            {@link Configuration}. Note that if this is a {@link TemplateConfiguration}, you will also want to call
+     *            {@link TemplateConfiguration#apply(Template)} on the resulting {@link Template} so that
      *            {@link Configurable} settings will be set too, because this constructor only uses it as a
-     *            {@link ParserConfiguration}.
+     *            {@link ParserConfiguration}.  
      * @param encoding
      *            Same as in {@link #Template(String, String, Reader, Configuration, String)}. When it's non-{@code
-     *            null}, it overrides the value coming from the {@link TemplateConfiguration#getEncoding()} method of
-     *            the {@code templateConfiguration} parameter.
+     *            null}, it overrides the value coming from the {@code TemplateConfiguration#getEncoding()} method of the
+     *            {@code templateConfigurer} parameter.
      * 
      * @since 2.3.24
      */
     public Template(
-            String name, String sourceName, Reader reader,
-            Configuration cfg, ParserConfiguration customParserConfiguration,
+            String name, String sourceName, Reader reader, Configuration cfg, ParserConfiguration customParserCfg,
             String encoding) throws IOException {
-        this(name, sourceName, cfg, customParserConfiguration);
-        
-        this.setEncoding(encoding);
-        LineTableBuilder ltbReader;
-        try {
-            if (!(reader instanceof BufferedReader) && !(reader instanceof StringReader)) {
-                reader = new BufferedReader(reader, 0x1000);
-            }
-            ltbReader = new LineTableBuilder(reader);
-            reader = ltbReader;
-            
-            try {
-                parser = new FMParser(this, reader, getParserConfiguration());
-                try {
-                    this.rootElement = parser.Root();
-                } catch (IndexOutOfBoundsException exc) {
-                    // There's a JavaCC bug where the Reader throws a RuntimeExcepton and then JavaCC fails with
-                    // IndexOutOfBoundsException. If that wasn't the case, we just rethrow. Otherwise we suppress the
-                    // IndexOutOfBoundsException and let the real cause to be thrown later. 
-                    if (!ltbReader.hasFailure()) {
-                        throw exc;
-                    }
-                    rootElement = null;
-                }
-                this.actualTagSyntax = parser._getLastTagSyntax();
-                this.actualNamingConvention = parser._getLastNamingConvention();
-            } 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();
-        }
-        
-        // Throws any exception that JavaCC has silently treated as EOF:
-        ltbReader.throwFailure();
-        
+        this(
+                _CoreAPI.newUnboundTemplate(
+                        reader,
+                        sourceName != null ? sourceName : name,
+                        toNonNull(cfg),
+                        customParserCfg,
+                        encoding),
+                name, cfg);
+        this.encoding = encoding;
         DebuggerService.registerTemplate(this);
-        namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
-        prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
     }
-
+    
     /**
      * Equivalent to {@link #Template(String, Reader, Configuration)
      * Template(name, reader, null)}.
@@ -295,19 +237,6 @@
     }
 
     /**
-     * Only meant to be used internally.
-     * 
-     * @deprecated Has problems setting actualTagSyntax and templateLanguageVersion; will be removed in 2.4.
-     */
-    @Deprecated
-    // [2.4] remove this
-    Template(String name, TemplateElement root, Configuration cfg) {
-        this(name, null, cfg, (ParserConfiguration) null);
-        this.rootElement = root;
-        DebuggerService.registerTemplate(this);
-    }
-    
-    /**
      * Same as {@link #getPlainTextTemplate(String, String, String, Configuration)} with {@code null} {@code sourceName}
      * argument.
      */
@@ -330,27 +259,11 @@
      * @since 2.3.22
      */
     static public Template getPlainTextTemplate(String name, String sourceName, String content, Configuration config) {
-        Template template;
-        try {
-            template = new Template(name, sourceName, new StringReader("X"), config);
-        } catch (IOException e) {
-            throw new BugException("Plain text template creation failed", e);
-        }
-        _CoreAPI.replaceText((TextBlock) template.rootElement, content);
-        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;
-        }
+        Template t = new Template(
+                _CoreAPI.newPlainTextUnboundTemplate(content, sourceName != null ? sourceName : name, config),
+                name, config);
+        DebuggerService.registerTemplate(t);
+        return t;
     }
 
     /**
@@ -514,6 +427,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
@@ -560,7 +481,7 @@
      * @since 2.3.22
      */
     public String getSourceName() {
-        return sourceName != null ? sourceName : getName();
+        return unboundTemplate.getSourceName();
     }
 
     /**
@@ -578,16 +499,18 @@
      * @since 2.3.24
      */
     public ParserConfiguration getParserConfiguration() {
-        return parserConfiguration;
+        return unboundTemplate.getParserConfiguration();
     }
     
     /**
      * 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();
     }
 
     /**
@@ -604,7 +527,16 @@
     }
 
     /**
-     * Returns the default character encoding used for reading included files.
+     * Returns the name of the charset used for reading included/imported template files by default.
+     * 
+     * <p>
+     * At least when FreeMarker is built-in template loading mechanism is used, by default this setting is set to the
+     * same value as the {@link UnboundTemplate#getTemplateSpecifiedEncoding()} of the wrapped {@link UnboundTemplate},
+     * if that's non-{@code null}.
+     * 
+     * <p>
+     * While "inheriting" charset from the referring template is not seen as a good idea anymore, it's still used by
+     * FreeMarker for backward compatibility (at least by default; as of 2.3.22 no setting exists yet to change that).
      */
     public String getEncoding() {
         return this.encoding;
@@ -643,7 +575,7 @@
      * @since 2.3.20
      */
     public int getActualTagSyntax() {
-        return actualTagSyntax;
+        return unboundTemplate.getActualTagSyntax();
     }
     
     /**
@@ -656,7 +588,7 @@
      * @since 2.3.23
      */
     public int getActualNamingConvention() {
-        return actualNamingConvention;
+        return unboundTemplate.getActualNamingConvention();
     }
     
     /**
@@ -669,14 +601,7 @@
      * @since 2.3.24
      */
     public OutputFormat getOutputFormat() {
-        return outputFormat;
-    }
-    
-    /**
-     * Meant to be called by the parser only. 
-     */
-    void setOutputFormat(OutputFormat outputFormat) {
-        this.outputFormat = outputFormat;
+        return unboundTemplate.getOutputFormat();
     }
     
     /**
@@ -689,48 +614,37 @@
      * @since 2.3.24
      */
     public boolean getAutoEscaping() {
-        return autoEscaping;
-    }
-
-    /**
-     * Meant to be called by the parser only. 
-     */
-    void setAutoEscaping(boolean autoEscaping) {
-        this.autoEscaping = autoEscaping;
+        return unboundTemplate.getAutoEscaping();
     }
 
     /**
      * 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.
      */
     @Deprecated
     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.
      */
     @Deprecated
-    public void addImport(LibraryLoad ll) {
-        imports.add(ll);
+    public void addImport(LibraryLoad libLoad) {
+        _CoreAPI.addImport(unboundTemplate, libLoad);
     }
 
     /**
@@ -741,136 +655,8 @@
      * @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;
-        StringBuilder buf = new StringBuilder();
-        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();
-    }
-
-    /**
-     * Reader that builds up the line table info for us, and also helps in working around JavaCC's exception
-     * suppression.
-     */
-    private class LineTableBuilder extends FilterReader {
-        
-        private final StringBuilder lineBuf = new StringBuilder();
-        int lastChar;
-        boolean closed;
-        
-        /** Needed to work around JavaCC behavior where it silently treats any errors as EOF. */ 
-        private Exception failure; 
-
-        /**
-         * @param r the character stream to wrap
-         */
-        LineTableBuilder(Reader r) {
-            super(r);
-        }
-        
-        public boolean hasFailure() {
-            return failure != null;
-        }
-
-        public void throwFailure() throws IOException {
-            if (failure != null) {
-                if (failure instanceof IOException) {
-                    throw (IOException) failure;
-                }
-                if (failure instanceof RuntimeException) {
-                    throw (RuntimeException) failure;
-                }
-                throw new UndeclaredThrowableException(failure);
-            }
-        }
-
-        @Override
-        public int read() throws IOException {
-            try {
-                int c = in.read();
-                handleChar(c);
-                return c;
-            } catch (Exception e) {
-                throw rememberException(e);
-            }
-        }
-
-        private IOException rememberException(Exception e) throws IOException {
-            // JavaCC used to read from the Reader after it was closed. So we must not treat that as a failure. 
-            if (!closed) {
-                failure = e;
-            }
-            if (e instanceof IOException) {
-                return (IOException) e;
-            }
-            if (e instanceof RuntimeException) {
-                throw (RuntimeException) e;
-            }
-            throw new UndeclaredThrowableException(e);
-        }
-
-        @Override
-        public int read(char cbuf[], int off, int len) throws IOException {
-            try {
-                int numchars = in.read(cbuf, off, len);
-                for (int i = off; i < off + numchars; i++) {
-                    char c = cbuf[i];
-                    handleChar(c);
-                }
-                return numchars;
-            } catch (Exception e) {
-                throw rememberException(e);
-            }
-        }
-
-        @Override
-        public void close() throws IOException {
-            if (lineBuf.length() > 0) {
-                lines.add(lineBuf.toString());
-                lineBuf.setLength(0);
-            }
-            super.close();
-            closed = true;
-        }
-
-        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);
     }
 
     /**
@@ -878,15 +664,35 @@
      */
     @Deprecated
     public TemplateElement getRootTreeNode() {
-        return rootElement;
+        return _CoreAPI.getRootTreeNode(unboundTemplate);
     }
     
     /**
+     * For 2.3 backward compatibility. Initialized on demand.
+     */
+    private volatile Map<String, Macro> legacyMacroMap;
+    
+    /**
+     * Returns the {@link Map} that maps the macro names to the actual macros. This map shouldn't be modified; if you
+     * absolutely has to use these deprecated API-s for adding a macro, at least use {@link #addMacro(Macro)}.
+     * (Specifying the {@link Map} key has no purpose anyway, as the macro will be always defined with its original
+     * name, as returned by {@link Macro#getName()}.) 
+     * 
      * @deprecated Should only be used internally, and might will be removed later.
      */
     @Deprecated
     public Map getMacros() {
-        return macros;
+        Map<String, Macro> legacyMacroMap = this.legacyMacroMap;
+        if (legacyMacroMap == null) {
+            synchronized (this) {
+                legacyMacroMap = this.legacyMacroMap;
+                if (legacyMacroMap == null) {
+                    legacyMacroMap = _CoreAPI.createAdapterMacroMapForUnboundCallables(unboundTemplate);
+                    this.legacyMacroMap = legacyMacroMap;
+                }
+            }
+        }
+        return legacyMacroMap;
     }
 
     /**
@@ -894,7 +700,7 @@
      */
     @Deprecated
     public List getImports() {
-        return imports;
+        return _CoreAPI.getImports(unboundTemplate);
     }
 
     /**
@@ -904,57 +710,25 @@
      */
     @Deprecated
     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);
     }
     
     /**
@@ -963,47 +737,27 @@
      * 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.
      */
     @Deprecated
     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 {
@@ -1055,4 +809,3 @@
     }
 
 }
-
diff --git a/src/main/java/freemarker/template/TemplateException.java b/src/main/java/freemarker/template/TemplateException.java
index 7845bf8..96fb5b7 100644
--- a/src/main/java/freemarker/template/TemplateException.java
+++ b/src/main/java/freemarker/template/TemplateException.java
@@ -33,6 +33,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;
@@ -199,11 +200,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 = Integer.valueOf(templateObject.getBeginLine());
                     columnNumber = Integer.valueOf(templateObject.getBeginColumn());
                     endLineNumber = Integer.valueOf(templateObject.getEndLine());
@@ -214,6 +215,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 765383e..8ca2642 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -19,6 +19,7 @@
 
 package freemarker.template;
 
+import java.util.Locale;
 import java.util.Set;
 
 import freemarker.cache.CacheStorage;
@@ -26,8 +27,8 @@
 import freemarker.cache.TemplateLookupStrategy;
 import freemarker.cache.TemplateNameFormat;
 import freemarker.core.Expression;
-import freemarker.core.OutputFormat;
 import freemarker.core.TemplateObject;
+import freemarker.core.UnboundTemplate;
 import freemarker.template.utility.NullArgumentException;
 
 /**
@@ -44,7 +45,7 @@
     public static final int VERSION_INT_2_3_22 = Configuration.VERSION_2_3_22.intValue();
     public static final int VERSION_INT_2_3_23 = Configuration.VERSION_2_3_23.intValue();
     public static final int VERSION_INT_2_3_24 = Configuration.VERSION_2_3_24.intValue();
-    public static final int VERSION_INT_2_4_0 = Version.intValueFor(2, 4, 0);
+    public static final int VERSION_INT_2_4_0 = Configuration.VERSION_2_4_0.intValue();   
     
     public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) {
         NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
@@ -54,19 +55,21 @@
                     + incompatibleImprovements + ", but the installed FreeMarker version is only "
                     + Configuration.getVersion() + ". You may need to upgrade FreeMarker in your project.");
         }
+        if (iciV > Configuration.VERSION_2_3_24.intValue() && iciV < Configuration.VERSION_2_4_0.intValue()) {
+            throw new IllegalArgumentException("The FreeMarker version requested by \"incompatibleImprovements\" was "
+                    + incompatibleImprovements + ", but the installed FreeMarker version ("
+                    + Configuration.getVersion() + ") doesn't know a such high 2.3.x version. "
+                    + "You may need to upgrade FreeMarker in your project.");
+        }
         if (iciV < VERSION_INT_2_3_0) {
             throw new IllegalArgumentException("\"incompatibleImprovements\" must be at least 2.3.0.");
         }
     }
     
     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();
@@ -100,16 +103,17 @@
     /**
      * [FM 2.4] getSettingNames() becomes to public; remove this.
      */
-    public static Set/*<String>*/ getConfigurationSettingNames(Configuration cfg, boolean camelCase) {
+    public static Set<String> getConfigurationSettingNames(Configuration cfg, boolean camelCase) {
         return cfg.getSettingNames(camelCase);
     }
     
-    public static void setAutoEscaping(Template t, boolean autoEscaping) {
-        t.setAutoEscaping(autoEscaping);
-    }
-    
-    public static void setOutputFormat(Template t, OutputFormat outputFormat) {
-        t.setOutputFormat(outputFormat);
+    /** Eventually, this constructor should become public, and then we don't need this anymore. */
+    public static Template unboundTemplateToTemplate(UnboundTemplate unboundTemplate,
+            String name, Locale locale, Object customLookupCondition,
+            Configuration cfg) {
+        return new Template(unboundTemplate,
+                name, locale, customLookupCondition,
+                cfg);
     }
 
     public static void validateAutoEscapingPolicyValue(int autoEscaping) {
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index fe8d753..4ddd741 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -25,6 +25,7 @@
 import freemarker.core.Environment;
 import freemarker.core.Macro;
 import freemarker.core.TemplateMarkupOutputModel;
+import freemarker.core._CoreAPI;
 import freemarker.ext.beans.BeanModel;
 import freemarker.ext.beans.BooleanModel;
 import freemarker.ext.beans.CollectionModel;
@@ -291,7 +292,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 38e1b91..83747a9 100644
--- a/src/main/java/freemarker/template/utility/CollectionUtils.java
+++ b/src/main/java/freemarker/template/utility/CollectionUtils.java
@@ -26,6 +26,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 58d5ffb..cb8a438 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -556,7 +556,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 b800eb4..6757894 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
Binary files differ
diff --git a/src/main/resources/freemarker/version.properties b/src/main/resources/freemarker/version.properties
index e209196..4080c2b 100644
--- a/src/main/resources/freemarker/version.properties
+++ b/src/main/resources/freemarker/version.properties
@@ -57,11 +57,11 @@
 #   continue working without modification or recompilation.
 # - When the major version number is increased, major backward
 #   compatibility violations are allowed, but still should be avoided.
-version=2.3.24-rc01-incubating
+version=2.4.0-nightly_@timestampInVersion@-incubating
 # This exists as oss.sonatype only allows SNAPSHOT and final releases,
 # so instead 2.3.21-rc01 and such we have to use 2.3.21-SNAPSHOT there.
 # For final releases it's the same as "version".
-mavenVersion=2.3.24-rc01-incubating
+mavenVersion=2.4.0-SNAPSHOT
 
 # Version string that conforms to OSGi
 # ------------------------------------
@@ -75,7 +75,7 @@
 #   2.4.0.pre01
 #   2.4.0.nightly_@timestampInVersion@
 # During Apache Incubation, "-incubating" is added to this string.
-versionForOSGi=2.3.24.rc01-incubating
+versionForOSGi=2.4.0.nightly_@timestampInVersion@-incubating
 
 # Version string that conforms to legacy MF
 # -----------------------------------------
@@ -93,10 +93,11 @@
 # "97 denotes "nightly", 98 denotes "pre", 99 denotes "rc" build.
 # In general, for the nightly/preview/rc Y of version 2.X, the versionForMf is
 # 2.X-1.(99|98).Y. Note the X-1.
-versionForMf=2.3.23.99.1
+versionForMf=2.3.97
 
 # The date of the build.
 # This should be automatically filled by the building tool (Ant).
 buildTimestamp=@timestampNice@
 
+# 2.4.0+ must be always GAE compliant, but we keep this here for B.C.
 isGAECompliant=true
diff --git a/src/manual/change-log-2.4.txt b/src/manual/change-log-2.4.txt
new file mode 100644
index 0000000..1f58955
--- /dev/null
+++ b/src/manual/change-log-2.4.txt
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+This is the (draft) of the 2.4.0 change log so far. It's not in the Manual yet, as merging in XDocBook XML
+changes from the 2.3 branch would be rather difficult.
+
+Non-backward compatible (all are unlikely to affect real world applications):
+- Removed command-line tools (main methods in a library, often classified as CWE-489 "Leftover Debug Code"):
+  - freemarker.core.CommandLine (jar main-class). This has only printed the current version and copyright.
+  - freemarker.ext.dom.Transform. This was used to transform an XML file to output file via a template file.
+    Surpassed by FMPP long ago. (If there will be demand for it, it can be still reintroduced in a separate jar.)
+  - freemarker.template.utility.ToCanonical class: Converted a template to its canonical form. As the canonical
+    form was heavily broken before 2.3.21, and is still not perfect, yet nobody has complained, it's assumed
+    that canonicalization isn't used, hence nor the CLI to it.
+  - freemarker.ext.jdom.NodeListModel.main() method: Was used to transform an XML given on stdin with the specified
+    template file and write the output to stdout. The data model used for this transformation (JDOM NodeListModel) was
+    deprecated long ago.
+- Removed TemplateObject.getTemplate(), added getUnboundTemplate() instead. TemplateObject is not to be confused with
+  Template, and is only exploited by very few for some deeper tricks. Although TemplateObject is technically
+  (historically) public, it was marked as internal API and was excluded from the JavaDoc for a while.
+- When TemplateCache2 will be merged in: In 2.3.x, a cached error was thrown as a plain IOException with the original
+  (cached) exception as its cause exception.
+  This is not possible starting from 2.4.x, because to prevent memory leaks, the cache doesn't hold reference to the
+  cached exception any more, instead it only stores a String description of it (class name, message, and the same for
+  each its cause exceptions). The cached error will be throw as a CachedTemplateLoadingException (an IOException
+  subclass), which contains the string description of the original exception in its getMessage() value.
+- Removed deprecated *package visible* method:
+  Template#Template(String name, TemplateElement root, Configuration cfg)
+- freemarker.core.FMParser API changes. These were marked as internal and deprecated for a while,
+  and are very unlikely to be used. They could be exploited for tools that try to tokenize String-s as FTL or such.
+  - freemarker.core.FMParser constructors were removed or has become non-public
+  - freemarker.core.FMParser.createExpressionParser war removed
+
+New features:
+- There are no separate org.freemarker:freemarker-gae and org.freemarker:freemarker artifacts any more.
+  There's only org.freemarker:freemarker, which is GAE compatible.
+- TemplateLoader.getLastModified(Object templateSource) can now throw GetLastModifiedException to indicate an
+  error instead of returning -1 (which doesn't count as an error).
+- Custom attributes now keep the order of adding them
+- Bug fixed: When the same template was both #include-d and #import-ed, the macros from the #import-ed template has
+  ran in the namespace of the #import-ed template.
+
+Notes on internal changes (some of this need not be in Version History):
+- The main change is splitting Template to Template and UnboundTemplate. With an analogy, if UnboundTemplate is
+  the class of the template, then Template is the instance of the template. The content of UnboundTemplate only
+  depends on the content of the actual template file, the ParserSettings, and on its *source* path, while Template
+  contains all the settings defined in by the Configurable class and the name with which the template was requested.
+  This splitting allows more efficient caching when due to the template lookup mechanism, multiple Template-s
+  share the same actual template file.
diff --git a/src/test/java/freemarker/core/CustomAttributeInUnboundTemplatesTest.java b/src/test/java/freemarker/core/CustomAttributeInUnboundTemplatesTest.java
new file mode 100644
index 0000000..4452dcd
--- /dev/null
+++ b/src/test/java/freemarker/core/CustomAttributeInUnboundTemplatesTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+
+@SuppressWarnings("boxing")
+public class CustomAttributeInUnboundTemplatesTest {
+
+    @Test
+    public void inFtlHeaderTest() throws IOException {
+        Template t = new Template(null, "<#ftl attributes={'a': 1?int}>", new Configuration(Configuration.VERSION_2_3_23));
+        t.setCustomAttribute("b", 2);
+        assertEquals(1, t.getCustomAttribute("a"));
+        assertEquals(2, t.getCustomAttribute("b"));
+        assertEquals(ImmutableMap.of("a", 1), t.getUnboundTemplate().getCustomAttributes());
+    }
+    
+    @Test
+    public void inTemplateConfigurationTest() throws IOException {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
+        
+        TemplateConfiguration tc = new TemplateConfiguration();
+        tc.setCustomAttribute("a", 1);
+        tc.setParentConfiguration(cfg);
+        
+        Template t = new Template(null, null, new StringReader(""), cfg, tc, null);
+        t.setCustomAttribute("b", 2);
+        assertNull(t.getCustomAttribute("a"));
+        tc.apply(t);
+        assertEquals(1, t.getCustomAttribute("a"));
+        assertEquals(2, t.getCustomAttribute("b"));
+        assertNull(t.getUnboundTemplate().getCustomAttributes());
+    }
+
+}
diff --git a/src/test/java/freemarker/core/DirectiveCallPlaceTest.java b/src/test/java/freemarker/core/DirectiveCallPlaceTest.java
index 6fc03f3..db93d8d 100644
--- a/src/test/java/freemarker/core/DirectiveCallPlaceTest.java
+++ b/src/test/java/freemarker/core/DirectiveCallPlaceTest.java
@@ -198,7 +198,7 @@
             Writer out = env.getOut();
             DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace();
             out.write("[");
-            out.write(getTemplateSourceName(callPlace));
+            out.write(callPlace.getUnboundTemplate().getSourceName());
             out.write(":");
             out.write(Integer.toString(callPlace.getBeginLine()));
             out.write(":");
@@ -212,10 +212,6 @@
                 body.render(out);
             }
         }
-
-        private String getTemplateSourceName(DirectiveCallPlace callPlace) {
-            return ((UnifiedCall) callPlace).getTemplate().getSourceName();
-        }
         
     }
 
diff --git a/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java b/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java
index dc6eb34..1f6ec27 100644
--- a/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java
+++ b/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java
@@ -123,6 +123,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 =
@@ -132,31 +145,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"
@@ -165,19 +178,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));
@@ -197,6 +210,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 iciVersion) {
         Configuration cfg = new Configuration(iciVersion);
@@ -212,8 +234,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 9ff5152..b4795c0 100644
--- a/src/test/java/freemarker/template/CustomAttributeTest.java
+++ b/src/test/java/freemarker/template/CustomAttributeTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.*;
 
 import java.math.BigDecimal;
-import java.util.Arrays;
 
 import org.junit.Test;
 
@@ -65,17 +64,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));
         
@@ -105,7 +104,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));
         
@@ -115,7 +114,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', "
@@ -123,20 +122,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', "
@@ -144,21 +143,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
@@ -205,6 +211,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());
@@ -215,13 +273,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/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index f94f2d9..863a9c5 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -81,11 +81,16 @@
         }
         expected.add(Configuration.VERSION_2_3_21);
         expected.add(Configuration.VERSION_2_3_22);
-        expected.add(Configuration.VERSION_2_3_22); // no non-BC change in 2.3.23
+        expected.add(Configuration.VERSION_2_3_22);
+        expected.add(Configuration.VERSION_2_3_24);
         expected.add(Configuration.VERSION_2_3_24);
 
         List<Version> actual = new ArrayList<Version>();
         for (int i = _TemplateAPI.VERSION_INT_2_3_0; i <= Configuration.getVersion().intValue(); i++) {
+            if (i > _TemplateAPI.VERSION_INT_2_3_24 && i < _TemplateAPI.VERSION_INT_2_4_0) {
+                continue;
+            }
+            
             int major = i / 1000000;
             int minor = i % 1000000 / 1000;
             int micro = i % 1000;
diff --git a/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
index 6395263..6ef099c 100644
--- a/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
+++ b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
@@ -62,7 +62,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());
             }
         }
@@ -92,14 +92,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/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java b/src/test/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java
index aa357f5..9ce51ca 100644
--- a/src/test/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java
+++ b/src/test/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java
@@ -25,6 +25,7 @@
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.Map;
+import java.util.Set;
 
 import org.junit.Test;
 
@@ -56,6 +57,25 @@
         
         assertEquals("123b 1b23b", getTemplateOutput(t));
     }
+    
+    /**
+     * Same as {@link #testMacroCopyingExploit()}, but to make it worse, it adds the macros directly through the macro
+     * {@link Map}. 
+     */
+    @Test
+    public void testMacroCopyingExploitWithMapModification() throws IOException, TemplateException {
+        Template tMacros = new Template(null, "<#macro m1>1</#macro><#macro m2>2</#macro>", cfg);
+        Map<String, Macro> macros = tMacros.getMacros();
+        
+        Template t = new Template(null,
+                "<@m1/><@m2/><@m3/>"
+                + "<#macro m1>1b</#macro><#macro m3>3b</#macro> "
+                + "<@m1/><@m2/><@m3/>", cfg);
+        t.getMacros().put("m1", macros.get("m1"));
+        t.getMacros().put("whatever", macros.get("m2"));  // Legacy bug: Map key doesn't mater, only original macro name
+        
+        assertEquals("123b 1b23b", getTemplateOutput(t));
+    }    
 
     @Test
     public void testMacroCopyingExploitAndNamespaces() throws IOException, TemplateException {
@@ -79,6 +99,34 @@
         
         assertEquals("1", getTemplateOutput(t));
     }
+
+    /**
+     * Same as {@link #testMacroCopyingFromFTLVariable()}, but to make it worse, it adds the macros directly through the
+     * {@link Map}.
+     */
+    @Test
+    public void testMacroCopyingFromFTLVariableWithMapModification() throws IOException, TemplateException {
+        Template tMacros = new Template(null, "<#assign x = 0><#macro m1>${x}</#macro>", cfg);
+        Environment env = tMacros.createProcessingEnvironment(null, NullWriter.INSTANCE);
+        env.process();
+        TemplateModel m1 = env.getVariable("m1");
+        assertThat(m1, instanceOf(Macro.class));
+        
+        for (int variation : new int[] { 1, 2 }) {
+            Template t = new Template(null, "<#assign x = 1><@m1/>", cfg);
+            if (variation == 1) {
+                t.getMacros().put("m1", m1);
+            } else {
+                t.getMacros().put("m1", null); // Just so it appears in the Entry Set.
+                for (Map.Entry<String, Macro> ent : (Set<Map.Entry>) t.getMacros().entrySet()) {
+                    if (ent.getKey().equals("m1")) {
+                        ent.setValue((Macro) m1);
+                    }
+                }
+            }
+            assertEquals("1", getTemplateOutput(t));
+        }
+    }
     
     private String getTemplateOutput(Template t) throws TemplateException, IOException {
         StringWriter sw = new StringWriter();
diff --git a/src/test/resources/freemarker/core/ast-1.ast b/src/test/resources/freemarker/core/ast-1.ast
index 2956157..a0d4e06 100644
--- a/src/test/resources/freemarker/core/ast-1.ast
+++ b/src/test/resources/freemarker/core/ast-1.ast
@@ -112,7 +112,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
@@ -129,7 +129,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/core/ast-assignments.ast b/src/test/resources/freemarker/core/ast-assignments.ast
index 2191544..3377ce1 100644
--- a/src/test/resources/freemarker/core/ast-assignments.ast
+++ b/src/test/resources/freemarker/core/ast-assignments.ast
@@ -92,7 +92,7 @@
             - assignment source: 2  // f.c.NumberLiteral
             - variable scope: "3"  // Integer
             - namespace: null  // Null
-    #macro  // f.c.Macro
+    #macro  // f.c.UnboundCallable
         - assignment target: "m"  // String
         - catch-all parameter name: null  // Null
         - AST-node subtype: "0"  // Integer
@@ -169,4 +169,4 @@
             - assignment operator: "--"  // String
             - assignment source: null  // Null
             - variable scope: "1"  // Integer
-            - namespace: null  // Null
\ No newline at end of file
+            - namespace: 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 fe45a0c..427b70b 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
@@ -35,4 +35,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 />