You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2019/10/23 22:08:06 UTC

[freemarker] branch 2.3-gae updated (54905b6 -> 7e6b0c0)

This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a change to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git.


    from 54905b6  Added ?spread_args, though it's likely not the final version
     new 6ea15af  Added a new SimpleHash constructor, where the caller can provide the Map instance used as the backing storage, thus allows controlling the ordering, and other technical aspects (like the initial capacity) of the underlying Map.
     new ccae393  Macro catch-all parameters (aka. macro varargs), when capture arguments passed by name (as opposed to by position), now keep the order of arguments.
     new 7e6b0c0  Added new special variable, .args. This evaluates to a hash (in macros), or sequence (in functions) that contains all the arguments. This is useful for operations that act on all the arguments uniformly, like for example to pass the arguments to ?spread_args(...).

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/main/java/freemarker/core/BuiltinVariable.java |  30 ++--
 src/main/java/freemarker/core/Environment.java     |   3 +-
 src/main/java/freemarker/core/Macro.java           | 160 ++++++++++++++++-----
 src/main/java/freemarker/template/SimpleHash.java  |  22 +++
 src/main/javacc/FTL.jj                             |  13 +-
 src/manual/en_US/book.xml                          | 125 +++++++++++++++-
 .../freemarker/core/ArgsSpecialVariableTest.java   | 160 +++++++++++++++++++++
 7 files changed, 465 insertions(+), 48 deletions(-)
 create mode 100644 src/test/java/freemarker/core/ArgsSpecialVariableTest.java


[freemarker] 02/03: Macro catch-all parameters (aka. macro varargs), when capture arguments passed by name (as opposed to by position), now keep the order of arguments.

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit ccae3935a6e8a32a11225898d462af591e296f1f
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Oct 23 20:57:29 2019 +0200

    Macro catch-all parameters (aka. macro varargs), when capture arguments passed by name (as opposed to by position), now keep the order of arguments.
---
 src/main/java/freemarker/core/Environment.java |  3 ++-
 src/manual/en_US/book.xml                      | 16 +++++++++++-----
 2 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index a048681..13ef380 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -36,6 +36,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -1047,7 +1048,7 @@ public final class Environment extends Configurable {
 
     private static SimpleHash initNamedCatchAllParameter(Macro.Context macroCtx, String catchAllParamName) {
         SimpleHash namedCatchAllParamValue;
-        namedCatchAllParamValue = new SimpleHash((ObjectWrapper) null);
+        namedCatchAllParamValue = new SimpleHash(new LinkedHashMap<String, Object>(), null, 0);
         macroCtx.setLocalVar(catchAllParamName, namedCatchAllParamValue);
         return namedCatchAllParamValue;
     }
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 53557df..83e7a50 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -22831,11 +22831,10 @@ or
       myns:bar=7</programlisting>
 
           <warning>
-            <para>Currently, named catch-all parameters are unordered, that
-            is, you don't know what order will they be enumerated. That is,
-            they aren't returned in the same order as they were passed in
-            (that above example output shows them in the same order for
-            understandability only).</para>
+            <para>Before FreeMarker 2.3.30, named catch-all parameters are
+            unordered, that is, you don't know what order will they be
+            enumerated. Only starting from 2.3.30 are they returned in the
+            same order as they were passed in.</para>
           </warning>
 
           <section>
@@ -28769,6 +28768,13 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
             </listitem>
 
             <listitem>
+              <para><link linkend="ref.directive.macro">Macro catch-all
+              parameters</link> (aka. macro varargs), when capture arguments
+              passed by name (as opposed to by position), now keep the order
+              of arguments.</para>
+            </listitem>
+
+            <listitem>
               <para>Bug fixed: In <literal>&lt;#escape
               <replaceable>placeholder</replaceable> as
               <replaceable>escExpression</replaceable>&gt;</literal>, the


[freemarker] 01/03: Added a new SimpleHash constructor, where the caller can provide the Map instance used as the backing storage, thus allows controlling the ordering, and other technical aspects (like the initial capacity) of the underlying Map.

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 6ea15af5114666a459c434314d58574d3e200c03
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Oct 23 20:55:09 2019 +0200

    Added a new SimpleHash constructor, where the caller can provide the Map instance used as the backing storage, thus allows controlling the ordering, and other technical aspects (like the initial capacity) of the underlying Map.
---
 src/main/java/freemarker/template/SimpleHash.java | 22 ++++++++++++++++++++++
 src/manual/en_US/book.xml                         |  8 ++++++++
 2 files changed, 30 insertions(+)

diff --git a/src/main/java/freemarker/template/SimpleHash.java b/src/main/java/freemarker/template/SimpleHash.java
index c059c67..f32ebb2 100644
--- a/src/main/java/freemarker/template/SimpleHash.java
+++ b/src/main/java/freemarker/template/SimpleHash.java
@@ -115,6 +115,28 @@ public class SimpleHash extends WrappingTemplateModel implements TemplateHashMod
     }
 
     /**
+     * Creates an instance that will use the specified {@link Map} directly as its backing store; beware, the
+     * {@link Map} will be possibly modified by {@link SimpleHash}, even if you only read the {@link SimpleHash}.
+     * That's because when a value is read, it's replaced with the corresponding {@link TemplateModel}.
+     *
+     * <p>The goal of this constructor is to allow you to control technical aspects, like the initial capacity, and
+     * ordering of the underlying {@link Map} implementation. The iteration order of the {@link SimpleHash} will be
+     * the same as of the underlying {@link Map}.
+     *
+     * @param directMap The map that the instance will use directly as backing storage; possibly will be modified,
+     *                  even when you only read the {@link SimpleHash}! Must allow any kind of object as value,
+     *                  including {@code null}! Supporting {@code null} keys is not needed.
+     *
+     * @param overloadDistinction To avoid ambiguity with other overloads; the value is unused.
+     *
+     * @since 2.3.30
+     */
+    public SimpleHash(Map<String, Object> directMap, ObjectWrapper wrapper, int overloadDistinction) {
+        super(wrapper);
+        this.map = directMap;
+    }
+
+    /**
      * Creates a new hash by shallow-coping (possibly cloning) the underlying map; in many applications you should use
      * {@link DefaultMapAdapter} instead.
      *
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 2aa541d..53557df 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -28783,6 +28783,14 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
               <para>Bug fixed: AST traversal API now can properly traverse the
               inside of lambda expressions (such as the parameter list)</para>
             </listitem>
+
+            <listitem>
+              <para>Added a new <literal>SimpleHash</literal> constructor,
+              where the caller can provide the <literal>Map</literal> instance
+              used as the backing storage, thus allows controlling the
+              ordering, and other technical aspects (like the initial
+              capacity) of the underlying <literal>Map</literal>.</para>
+            </listitem>
           </itemizedlist>
         </section>
       </section>


[freemarker] 03/03: Added new special variable, .args. This evaluates to a hash (in macros), or sequence (in functions) that contains all the arguments. This is useful for operations that act on all the arguments uniformly, like for example to pass the arguments to ?spread_args(...).

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 7e6b0c0302e27ec767056c6930fbd8b88199e96e
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Oct 23 20:58:46 2019 +0200

    Added new special variable, .args. This evaluates to a hash (in macros), or sequence (in functions) that contains all the arguments. This is useful for operations that act on all the arguments uniformly, like for example to pass the arguments to ?spread_args(...).
---
 src/main/java/freemarker/core/BuiltinVariable.java |  30 ++--
 src/main/java/freemarker/core/Macro.java           | 160 ++++++++++++++++-----
 src/main/javacc/FTL.jj                             |  13 +-
 src/manual/en_US/book.xml                          | 101 +++++++++++++
 .../freemarker/core/ArgsSpecialVariableTest.java   | 160 +++++++++++++++++++++
 5 files changed, 422 insertions(+), 42 deletions(-)

diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java
index b42dcb3..7436dcb 100644
--- a/src/main/java/freemarker/core/BuiltinVariable.java
+++ b/src/main/java/freemarker/core/BuiltinVariable.java
@@ -78,8 +78,10 @@ final class BuiltinVariable extends Expression {
     static final String GET_OPTIONAL_TEMPLATE_CC = "getOptionalTemplate";
     static final String CALLER_TEMPLATE_NAME = "caller_template_name";
     static final String CALLER_TEMPLATE_NAME_CC = "callerTemplateName";
+    static final String ARGS = "args";
     static final String[] SPEC_VAR_NAMES = new String[] {
         // IMPORTANT! Keep this sorted alphabetically!
+        ARGS,
         AUTO_ESC_CC,
         AUTO_ESC,
         CALLER_TEMPLATE_NAME_CC,
@@ -256,21 +258,33 @@ final class BuiltinVariable extends Expression {
             return GetOptionalTemplateMethod.INSTANCE_CC;
         }
         if (name == CALLER_TEMPLATE_NAME || name == CALLER_TEMPLATE_NAME_CC) {
-            Context ctx = env.getCurrentMacroContext();
-            if (ctx == null) {
-                throw new TemplateException(
-                        "Can't get ." + name + " here, as there's no macro or function (that's "
-                        + "implemented in the template) call in context.", env);
-            }
-            TemplateObject callPlace = ctx.callPlace;
+            TemplateObject callPlace = getRequiredMacroContext(env).callPlace;
             String name = callPlace != null ? callPlace.getTemplate().getName() : null;
             return name != null ? new SimpleScalar(name) : TemplateScalarModel.EMPTY_STRING;
         }
-        
+        if (name == ARGS) {
+            TemplateModel args = getRequiredMacroContext(env).getArgsSpecialVariableValue();
+            if (args == null) {
+                // Should be impossible, as the parser checks this condition already.
+                throw new _MiscTemplateException(this, "The \"", ARGS, "\" special variable wasn't initialized.", name);
+            }
+            return args;
+        }
+
         throw new _MiscTemplateException(this,
                 "Invalid special variable: ", name);
     }
 
+    private Context getRequiredMacroContext(Environment env) throws TemplateException {
+        Context ctx = env.getCurrentMacroContext();
+        if (ctx == null) {
+            throw new TemplateException(
+                    "Can't get ." + name + " here, as there's no macro or function (that's "
+                    + "implemented in the template) call in context.", env);
+        }
+        return ctx;
+    }
+
     @Override
     public String toString() {
         return "." + name;
diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java
index 6527e80..b64e9e4 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -27,8 +27,11 @@ import java.util.List;
 import java.util.Map;
 
 import freemarker.template.Configuration;
+import freemarker.template.SimpleHash;
+import freemarker.template.SimpleSequence;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateModelIterator;
@@ -45,7 +48,7 @@ public final class Macro extends TemplateElement implements TemplateModel {
 
     static final Macro DO_NOTHING_MACRO = new Macro(".pass", 
             Collections.EMPTY_MAP,
-            null, false,
+            null, false, false,
             TemplateElements.EMPTY);
     
     final static int TYPE_MACRO = 0;
@@ -55,6 +58,7 @@ public final class Macro extends TemplateElement implements TemplateModel {
     private final String[] paramNames;
     private final Map<String, Expression> paramNamesWithDefault;
     private final SpreadArgs spreadArgs;
+    private boolean requireArgsSpecialVariable;
     private final String catchAllParamName;
     private final boolean function;
     private final Object namespaceLookupKey;
@@ -66,7 +70,7 @@ public final class Macro extends TemplateElement implements TemplateModel {
      */
     Macro(String name,
             Map<String, Expression> paramNamesWithDefault,
-            String catchAllParamName, boolean function,
+            String catchAllParamName, boolean function, boolean requireArgsSpecialVariable,
             TemplateElements children) {
         // Attention! Keep this constructor in sync with the other constructor!
         this.name = name;
@@ -74,6 +78,7 @@ public final class Macro extends TemplateElement implements TemplateModel {
         this.paramNames = paramNamesWithDefault.keySet().toArray(new String[0]);
         this.catchAllParamName = catchAllParamName;
         this.spreadArgs = null;
+        this.requireArgsSpecialVariable = requireArgsSpecialVariable;
         this.function = function;
         this.setChildren(children);
         this.namespaceLookupKey = this;
@@ -82,7 +87,7 @@ public final class Macro extends TemplateElement implements TemplateModel {
 
     /**
      * Copy-constructor with replacing {@link #spreadArgs} (with the quirk that the parent of the
-     * child elements will stay the copied macro).
+     * child AST elements will stay the copied macro).
      *
      * @param spreadArgs Usually {@code null}; used by {@link BuiltInsForCallables.spread_argsBI} to
      *      set arbitrary default value to parameters. Note that the defaults aren't
@@ -95,12 +100,17 @@ public final class Macro extends TemplateElement implements TemplateModel {
         this.paramNames = that.paramNames;
         this.catchAllParamName = that.catchAllParamName;
         this.spreadArgs = spreadArgs; // Using the argument value here
+        this.requireArgsSpecialVariable = that.requireArgsSpecialVariable;
         this.function = that.function;
         this.namespaceLookupKey = that.namespaceLookupKey;
         super.copyFieldsFrom(that);
         // Attention! Keep this constructor in sync with the other constructor!
     }
 
+    boolean getRequireArgsSpecialVariable() {
+        return requireArgsSpecialVariable;
+    }
+
     public String getCatchAll() {
         return catchAllParamName;
     }
@@ -210,6 +220,7 @@ public final class Macro extends TemplateElement implements TemplateModel {
         final List<String> nestedContentParameterNames;
         final LocalContextStack prevLocalContextStack;
         final Context prevMacroContext;
+        TemplateModel argsSpecialVariableValue;
         
         Context(Environment env, 
                 TemplateObject callPlace,
@@ -221,41 +232,55 @@ public final class Macro extends TemplateElement implements TemplateModel {
             this.prevLocalContextStack = env.getLocalContextStack();
             this.prevMacroContext = env.getCurrentMacroContext();
         }
-                
-        
+
         Macro getMacro() {
             return Macro.this;
         }
 
-        // Set default parameters, check if all the required parameters are defined.
+        /**
+         * Set default parameters, check if all the required parameters are defined. Also sets the value of
+         * {@code .args}, if that was requested.
+         */
         void checkParamsSetAndApplyDefaults(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) {
+            boolean resolvedADefaultValue, hasUnresolvedDefaultValue;
+            Expression firstUnresolvedDefaultValueExpression;
+            InvalidReferenceException firstInvalidReferenceExceptionForDefaultValue;
+
+            final TemplateModel[] argsSpecVarDraft;
+            if (Macro.this.requireArgsSpecialVariable) {
+                argsSpecVarDraft = new TemplateModel[paramNames.length];
+            } else {
+                argsSpecVarDraft = null;
+            }
+            do { // Retried if there are unresolved defaults left
+                firstUnresolvedDefaultValueExpression = null;
+                firstInvalidReferenceExceptionForDefaultValue = null;
+                resolvedADefaultValue = hasUnresolvedDefaultValue = false;
+                for (int paramIndex = 0; paramIndex < paramNames.length; ++paramIndex) {
+                    final String argName = paramNames[paramIndex];
+                    final TemplateModel argValue = localVars.get(argName);
+                    if (argValue == null) {
                         Expression defaultValueExp = paramNamesWithDefault.get(argName);
                         if (defaultValueExp != null) {
                             try {
-                                TemplateModel tm = defaultValueExp.eval(env);
-                                if (tm == null) {
-                                    if (!hasUnresolvedArg) {
-                                        firstUnresolvedExpression = defaultValueExp;
-                                        hasUnresolvedArg = true;
+                                TemplateModel defaultValue = defaultValueExp.eval(env);
+                                if (defaultValue == null) {
+                                    if (!hasUnresolvedDefaultValue) {
+                                        firstUnresolvedDefaultValueExpression = defaultValueExp;
+                                        hasUnresolvedDefaultValue = true;
                                     }
                                 } else {
-                                    localVars.put(argName, tm);
-                                    resolvedAnArg = true;
+                                    localVars.put(argName, defaultValue);
+                                    resolvedADefaultValue = true;
+
+                                    if (argsSpecVarDraft != null) {
+                                        argsSpecVarDraft[paramIndex] = defaultValue;
+                                    }
                                 }
                             } catch (InvalidReferenceException e) {
-                                if (!hasUnresolvedArg) {
-                                    hasUnresolvedArg = true;
-                                    firstReferenceException = e;
+                                if (!hasUnresolvedDefaultValue) {
+                                    hasUnresolvedDefaultValue = true;
+                                    firstInvalidReferenceExceptionForDefaultValue = e;
                                 }
                             }
                         } else if (!env.isClassicCompatible()) {
@@ -265,7 +290,7 @@ public final class Macro extends TemplateElement implements TemplateModel {
                                             "When calling ", (isFunction() ? "function" : "macro"), " ",
                                             new _DelayedJQuote(name), 
                                             ", required parameter ", new _DelayedJQuote(argName),
-                                            " (parameter #", Integer.valueOf(i + 1), ") was ", 
+                                            " (parameter #", Integer.valueOf(paramIndex + 1), ") was ",
                                             (argWasSpecified
                                                     ? "specified, but had null/missing value."
                                                     : "not specified.") 
@@ -281,16 +306,79 @@ public final class Macro extends TemplateElement implements TemplateModel {
                                                     + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" }
                                             ));
                         }
+                    } else if (argsSpecVarDraft != null) {
+                        // Minor performance problem here: If there are multiple iterations due to default value
+                        // dependencies, this will set many parameters for multiple times.
+                        argsSpecVarDraft[paramIndex] = argValue;
                     }
                 }
-            } while (resolvedAnArg && hasUnresolvedArg);
-            if (hasUnresolvedArg) {
-                if (firstReferenceException != null) {
-                    throw firstReferenceException;
+            } while (hasUnresolvedDefaultValue && resolvedADefaultValue);
+            if (hasUnresolvedDefaultValue) {
+                if (firstInvalidReferenceExceptionForDefaultValue != null) {
+                    throw firstInvalidReferenceExceptionForDefaultValue;
                 } else if (!env.isClassicCompatible()) {
-                    throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env);
+                    throw InvalidReferenceException.getInstance(firstUnresolvedDefaultValueExpression, env);
                 }
             }
+            
+            if (argsSpecVarDraft != null) {
+                final String catchAllParamName = getMacro().catchAllParamName;
+                final TemplateModel catchAllArgValue = catchAllParamName != null ? localVars.get(catchAllParamName) : null;
+
+                if (getMacro().isFunction()) {
+                    int lengthWithCatchAlls = argsSpecVarDraft.length;
+                    if (catchAllArgValue != null) {
+                        lengthWithCatchAlls += ((TemplateSequenceModel) catchAllArgValue).size();
+                    }
+
+                    SimpleSequence argsSpecVarValue = new SimpleSequence(lengthWithCatchAlls);
+                    for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) {
+                        argsSpecVarValue.add(argsSpecVarDraft[paramIndex]);
+                    }
+                    if (catchAllParamName != null) {
+                        TemplateSequenceModel catchAllSeq = (TemplateSequenceModel) catchAllArgValue;
+                        int catchAllSize = catchAllSeq.size();
+                        for (int j = 0; j < catchAllSize; j++) {
+                            argsSpecVarValue.add(catchAllSeq.get(j));
+                        }
+                    }
+                    assert argsSpecVarValue.size() == lengthWithCatchAlls;
+
+                    this.argsSpecialVariableValue = argsSpecVarValue;
+                } else { // #macro
+                    int lengthWithCatchAlls = argsSpecVarDraft.length;
+                    TemplateHashModelEx2 catchAllHash;
+                    if (catchAllParamName != null) {
+                        if (catchAllArgValue instanceof TemplateSequenceModel) {
+                            throw new _MiscTemplateException("The macro can only by called with named arguments, " +
+                                    "because it uses both .", BuiltinVariable.ARGS, " and catch-all parameter.");
+                        }
+                        catchAllHash = (TemplateHashModelEx2) catchAllArgValue;
+                        lengthWithCatchAlls += catchAllHash.size();
+                    } else {
+                        catchAllHash = null;
+                    }
+
+                    SimpleHash argsSpecVarValue = new SimpleHash(
+                            new LinkedHashMap<String, Object>(lengthWithCatchAlls * 4 / 3, 1.0f),
+                            null, 0);
+                    for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) {
+                        argsSpecVarValue.put(paramNames[paramIndex], argsSpecVarDraft[paramIndex]);
+                    }
+                    if (catchAllArgValue != null) {
+                        for (TemplateHashModelEx2.KeyValuePairIterator iter = catchAllHash.keyValuePairIterator();
+                                iter.hasNext(); ) {
+                            TemplateHashModelEx2.KeyValuePair kvp = iter.next();
+                            argsSpecVarValue.put(
+                                    ((TemplateScalarModel) kvp.getKey()).getAsString(),
+                                    kvp.getValue());
+                        }
+                    }
+                    assert argsSpecVarValue.size() == lengthWithCatchAlls;
+
+                    this.argsSpecialVariableValue = argsSpecVarValue;
+                }
+            } // if (argsSpecVarDraft != null)
         }
 
         public TemplateModel getLocalVariable(String name) throws TemplateModelException {
@@ -315,6 +403,14 @@ public final class Macro extends TemplateElement implements TemplateModel {
             }
             return result;
         }
+
+        TemplateModel getArgsSpecialVariableValue() {
+            return argsSpecialVariableValue;
+        }
+
+        void setArgsSpecialVariableValue(TemplateModel argsSpecialVariableValue) {
+            this.argsSpecialVariableValue = argsSpecialVariableValue;
+        }
     }
 
     @Override
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index 3880ab4..f69042f 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -92,7 +92,7 @@ public class FMParser {
      */
     private int continuableDirectiveNesting;
     
-    private boolean inMacro, inFunction;
+    private boolean inMacro, inFunction, requireArgsSpecialVariable;
     private LinkedList escapes = new LinkedList();
     private int mixedContentNesting; // for stripText
 
@@ -2118,6 +2118,13 @@ BuiltinVariable BuiltinVariable() :
             parseTimeValue = new SimpleScalar(outputFormat.getName());
         } else if (nameStr.equals(BuiltinVariable.AUTO_ESC) || nameStr.equals(BuiltinVariable.AUTO_ESC_CC)) {
             parseTimeValue = autoEscaping ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        } else if (nameStr.equals(BuiltinVariable.ARGS)) {
+            if (!inMacro && !inFunction) {
+                throw new ParseException("The \"" + BuiltinVariable.ARGS + "\" special variable must be "
+                        + "inside a macro or function in the template source code.", template, name);
+            }
+            requireArgsSpecialVariable = true;
+            parseTimeValue = null;
         } else {
             parseTimeValue = null;
         }
@@ -3456,6 +3463,7 @@ Macro Macro() :
             throw new ParseException("Macro or function definitions can't be nested into each other.", template, start);
         }
         if (isFunction) inFunction = true; else inMacro = true;
+        requireArgsSpecialVariable = false;
     }
     nameExp = IdentifierOrStringLiteral()
     {
@@ -3537,7 +3545,8 @@ Macro Macro() :
         }
         
         inMacro = inFunction = false;
-        Macro result = new Macro(name, paramNamesWithDefault, catchAllParamName, isFunction, children);
+        Macro result = new Macro(
+                name, paramNamesWithDefault, catchAllParamName, isFunction, requireArgsSpecialVariable, children);
         result.setLocation(template, start, end);
         template.addMacro(result);
         return result;
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 83e7a50..b680afc 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -24567,6 +24567,98 @@ There was no specific handler for node y
       <itemizedlist spacing="compact">
         <listitem>
           <para><indexterm>
+              <primary>args</primary>
+            </indexterm><literal>args</literal> (since 2.3.30): Used inside
+          the <link
+          linkend="ref.directive.macro"><literal>macro</literal></link> and
+          <link
+          linkend="ref.directive.function"><literal>function</literal></link>
+          directives, it returns all arguments of the current invocation of
+          the macro or function. This allows processing all the arguments in
+          an uniform way (like pass them to the <link
+          linkend="ref_builtin_spread_args"><literal>spread_args</literal>
+          built-in</link>). Further details:</para>
+
+          <itemizedlist>
+            <listitem>
+              <para>When used inside a macro, <literal>.args</literal> is
+              always a hash that maps the parameter names to the parameter
+              values. That's so even if the macro was called with positional
+              arguments, as the parameter names are declared by the
+              <literal>macro</literal> directive. But when used inside a
+              function, <literal>.args</literal> is always a sequence of the
+              argument values. That's because functions can only be called
+              with positional arguments, so the parameter names declared
+              inside the <literal>function</literal> directive aren't
+              relevant.</para>
+            </listitem>
+
+            <listitem>
+              <para>For arguments that have a default value (as
+              <literal>b</literal> in <literal>&lt;#macro m1 a
+              b=0&gt;</literal>), if the caller of the macro or function has
+              omitted that argument (as in <literal>&lt;@m1 a=1
+              /&gt;</literal>), <literal>.args</literal> will contain the
+              default value for that argument (<literal>{'a': 1, 'b':
+              0}</literal>).</para>
+            </listitem>
+
+            <listitem>
+              <para>If there's a catch-all argument, like
+              <literal>others</literal> in <literal>&lt;#macro m2 a, b,
+              others...&gt;</literal>, then <literal>.args</literal> will
+              contain the elements in the the catch-all parameter directly,
+              and won't contain the declared catch-all parameter
+              (<literal>others</literal>) itself. So for <literal>&lt;@m2 a=1
+              b=2 c=3 d=4 /&gt;</literal> it will be <literal>{'a': 1, 'b': 2,
+              'c': 3, 'd': 4}</literal>, and <emphasis>not</emphasis>
+              <literal>{'a': 1, 'b': 2, 'others': {'c':3, 'd':
+              4}}</literal>.</para>
+            </listitem>
+
+            <listitem>
+              <para>If a macro has a catch-all argument, and the macro uses
+              <literal>.args</literal> somewhere, it can only be called with
+              named arguments (like <literal>&lt;@m a=1 b=2 /&gt;</literal>),
+              and not with positional arguments (like <literal>&lt;@m 1 2
+              /&gt;</literal>). That's because for macros
+              <literal>.args</literal> is always a hash, but if the arguments
+              are positional, the catch-all arguments won't have names.</para>
+            </listitem>
+
+            <listitem>
+              <para><literal>.args</literal> is initialized when the macro or
+              function is called, and so it doesn't reflect changes made later
+              on the local variables that correspond to the arguments.</para>
+            </listitem>
+
+            <listitem>
+              <para><literal>.args</literal> must occur inside a
+              <literal>macro</literal> or <literal>function</literal>
+              directive on the source code level, or else it's a template
+              parsing error. So you can't use it in a template fragment that
+              you insert dynamically into a macro or function (like via the
+              <link linkend="ref.directive.include"><literal>include</literal>
+              directive</link> or <link
+              linkend="ref_builtin_eval"><literal>eval</literal>
+              built-in</link>). That's because the <literal>macro</literal> or
+              <literal>function</literal> has to know if it contains
+              <literal>.args</literal> earlier than it's called.</para>
+            </listitem>
+
+            <listitem>
+              <para>For macros, the order of elements in the
+              <literal>.args</literal> hash corresponds to the order as the
+              parameters were declared in the <literal>macro</literal>
+              directive, not to the order the caller has used. Except,
+              catch-all arguments will use the order that the caller
+              used.</para>
+            </listitem>
+          </itemizedlist>
+        </listitem>
+
+        <listitem>
+          <para><indexterm>
               <primary>auto_esc</primary>
             </indexterm><literal>auto_esc</literal> (since 2.3.24): Boolean
           value that tells if auto-escaping (based on output format) is on or
@@ -28755,6 +28847,15 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
               defaults. <link linkend="ref_builtin_spread_args">See more
               here...</link></para>
             </listitem>
+
+            <listitem>
+              <para>Added new <link linkend="ref_specvar">special
+              variable</link>, <literal>.args</literal>. This evaluates to a
+              hash (in macros), or sequence (in functions) that contains all
+              the arguments. This is useful for operations that act on all the
+              arguments uniformly, like for example to pass the arguments to
+              <literal>?spread_args(<replaceable>...</replaceable>)</literal>.</para>
+            </listitem>
           </itemizedlist>
         </section>
 
diff --git a/src/test/java/freemarker/core/ArgsSpecialVariableTest.java b/src/test/java/freemarker/core/ArgsSpecialVariableTest.java
new file mode 100644
index 0000000..1064744
--- /dev/null
+++ b/src/test/java/freemarker/core/ArgsSpecialVariableTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 org.junit.Test;
+
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class ArgsSpecialVariableTest extends TemplateTest {
+
+    @Test
+    public void macroSimpleTest() throws IOException, TemplateException {
+        String macroDef = "<#macro m a b><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>";
+        String expectedOutput = "a=11, b=22";
+        assertOutput(macroDef +
+                        "<@m a=11 b=22 />",
+                expectedOutput);
+        assertOutput(macroDef +
+                        "<@m 11 22 />",
+                expectedOutput);
+    }
+
+    @Test
+    public void macroWithDefaultsTest() throws IOException, TemplateException {
+        String macroDef = "<#macro m a b c=3><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>";
+        String expectedOutput = "" +
+                "a=11, b=22, c=33; " +
+                "a=11, b=22, c=3";
+        assertOutput(macroDef +
+                        "<@m a=11 b=22 c=33 />; " +
+                        "<@m a=11 b=22 />",
+                expectedOutput);
+        assertOutput(macroDef +
+                        "<@m 11 22 33 />; " +
+                        "<@m 11 22 />",
+                expectedOutput);
+    }
+
+    @Test
+    public void macroWithMultiPassDefaultsTest() throws IOException, TemplateException {
+        String macroDef = "<#macro m a=c b=c c=b><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>";
+        String expectedOutput = "" +
+                "a=33, b=33, c=33; " +
+                "a=22, b=22, c=22; " +
+                "a=11, b=33, c=33; " +
+                "a=11, b=22, c=22";
+        assertOutput(macroDef +
+                        "<@m c=33 />; " +
+                        "<@m b=22 />; " +
+                        "<@m a=11 c=33 />; " +
+                        "<@m a=11 b=22 />",
+                expectedOutput);
+        assertOutput(macroDef +
+                        "<@m null, null, 33 />; " +
+                        "<@m null, 22, null />; " +
+                        "<@m 11, null, 33 />; " +
+                        "<@m 11, 22, null />",
+                expectedOutput);
+    }
+
+    @Test
+    public void macroWithCatchAllTest() throws IOException, TemplateException {
+        assertOutput("" +
+                        "<#macro m a b=2 others...><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>" +
+                        "<@m a=11 b=22 c=33 d=44 />; " +
+                        "<@m a=11 b=22 />; " +
+                        "<@m a=11 />; " +
+                        "<@m a=11 c=33 />",
+                "a=11, b=22, c=33, d=44; " +
+                        "a=11, b=22; " +
+                        "a=11, b=2; " +
+                        "a=11, b=2, c=33");
+
+        assertErrorContains("" +
+                "<#macro m a b=2 others...><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>" +
+                "<@m 1, 2 />",
+                ".args", "catch-all");
+    }
+
+    @Test
+    public void functionSimpleTest() throws IOException, TemplateException {
+        String functionDef = "<#function f a b><#return .args?join(', ')></#function>";
+        String expectedOutput = "11, 22";
+        assertOutput(functionDef +
+                        "${f(11, 22)}",
+                expectedOutput);
+    }
+
+    @Test
+    public void functionWithDefaultsTest() throws IOException, TemplateException {
+        String functionDef = "<#function f a b c=3><#return .args?join(', ')></#function>";
+        String expectedOutput = "" +
+                "11, 22, 33; " +
+                "11, 22, 3";
+        assertOutput(functionDef +
+                        "${f(11, 22, 33)}; " +
+                        "${f(11, 22)}",
+                expectedOutput);
+    }
+
+    @Test
+    public void functionWithMultiPassDefaultsTest() throws IOException, TemplateException {
+        String functionDef = "<#function f a=c b=c c=b><#return .args?join(', ')></#function>";
+        assertOutput(functionDef +
+                        "${f(null, null, 33)}; " +
+                        "${f(null, 22)}; " +
+                        "${f(11, null, 33)}; " +
+                        "${f(11, 22)}",
+                "33, 33, 33; " +
+                        "22, 22, 22; " +
+                        "11, 33, 33; " +
+                        "11, 22, 22");
+        assertOutput(functionDef +
+                        "${f(11, 22)}; " +
+                        "${f(11, 22, 33)}",
+                "11, 22, 22; " +
+                        "11, 22, 33");
+    }
+
+    @Test
+    public void functionWithCatchAllTest() throws IOException, TemplateException {
+        assertOutput("" +
+                        "<#function f a b=2 others...><#return .args?join(', ')></#function>" +
+                        "${f(11, 22, 33, 44)}; " +
+                        "${f(11, 22)}; " +
+                        "${f(11)}; " +
+                        "${f(11, null, 33)}",
+                "11, 22, 33, 44; " +
+                        "11, 22; " +
+                        "11, 2; " +
+                        "11, 2, 33");
+    }
+
+    @Test
+    public void usedInWrongContextTest() throws IOException, TemplateException {
+        assertErrorContains("${.args}", "args", "macro", "function");
+        assertErrorContains("<#macro m>${'.args'?eval}</#macro><@m />", "args", "macro", "function");
+    }
+
+}