tons of progress on macros
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 26 Jun 2021 21:55:29 +0000 (14:55 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 27 Jun 2021 07:10:25 +0000 (00:10 -0700)
doc/flow-control.texi
src/language/control/define.c
src/language/lexer/macro.c
src/language/lexer/macro.h
tests/language/control/define.at

index 20644747dbf4dc0d6f2db40b69f1e0c90c473470..a3f39b6ccb8c40390b0b5126ec453e711fd9ddd4 100644 (file)
@@ -45,94 +45,142 @@ BREAK.
 @vindex DEFINE
 @cindex macro
 
+@subsection Overview
+
 @display
-DEFINE macro_name([argument[/argument]@dots{}])
-@dots{}body@dots{}
-!ENDDEFINE.
+@t{DEFINE} @i{macro_name}@t{(}@r{[}@i{argument}@r{[}@t{/}@i{argument}@r{]@dots{}]}@t{)}
+@dots{}@i{body}@dots{}
+@t{!ENDDEFINE.}
+@end display
 
-Each argument takes the following form:
-  @{!arg_name =,!POSITIONAL@} [!DEFAULT(default)] [!NOEXPAND]
-  @{!TOKENS(count),!CHAREND('token'),!ENCLOSE('start','end'),!CMDEND@}
+Each @i{argument} takes the following form:
+@display
+@r{@{}@i{!arg_name} @t{=},@t{!POSITIONAL}@r{@}}
+@r{[}@t{!DEFAULT(}@i{default}@t{)}@r{]}
+@r{[}@t{!NOEXPAND}@r{]}
+@r{@{}@t{!TOKENS(}@i{count}@t{)},@t{!CHAREND('}@i{token}@t{')},@t{!ENCLOSE('}@i{start}@t{','}@i{end}@t{')},@t{!CMDEND}@}
+@end display
 
-The following directives may be used within the body:
-  !OFFEXPAND
-  !ONEXPAND
+The following directives may be used within @i{body}:
+@example
+!OFFEXPAND
+!ONEXPAND
+@end example
 
 The following functions may be used within the body:
-  !BLANKS(count)
-  !CONCAT(arg@dots{})
-  !EVAL(arg)
-  !HEAD(arg)
-  !INDEX(haystack, needle)
-  !LENGTH(arg)
-  !NULL
-  !QUOTE(arg)
-  !SUBSTR(arg, start[, count])
-  !TAIL(arg)
-  !UNQUOTE(arg)
-  !UPCASE(arg)
+@display
+@t{!BLANKS(}@i{count}@t{)}
+@t{!CONCAT(}@i{arg}@dots{}@t{)}
+@t{!EVAL(}@i{arg}@t{)}
+@t{!HEAD(}@i{arg}@t{)}
+@t{!INDEX(}@i{haystack}@t{,} @i{needle}@t{)}
+@t{!LENGTH(}@i{arg}@t{)}
+@t{!NULL}
+@t{!QUOTE(}@i{arg}@t{)}
+@t{!SUBSTR(}@i{arg}@t{,} @i{start}[@t{,} @i{count}]@t{)}
+@t{!TAIL(}@i{arg}@t{)}
+@t{!UNQUOTE(}@i{arg}@t{)}
+@t{!UPCASE(}@i{arg}@t{)}
+@end display
 
 The body may also include the following constructs:
+@display
+@t{!IF (}@i{condition}@t{) !THEN} @i{true-expansion} @t{!ENDIF}
+@t{!IF (}@i{condition}@t{) !THEN} @i{true-expansion} @t{!ELSE} @i{false-expansion} @t{!ENDIF}
 
-  !IF (condition) !THEN true-expansion !ENDIF
-  !IF (condition) !THEN true-expansion !ELSE false-expansion !ENDIF
+@t{!DO} @i{!var} @t{=} @i{start} @t{!TO} @i{end} [@t{!BY} @i{step}]
+  @i{body}
+@t{!DOEND}
+@t{!DO} @i{!var} @t{!IN} @t{(}@i{expression}@t{)}
+  @i{body}
+@t{!DOEND}
 
-  !DO !var = start !TO end [!BY step]
-    body
-  !DOEND
-  !DO !var !IN (expression)
-    body
-  !DOEND
+@t{!LET} @i{!var} @t{=} @i{expression}
 @end display
 
-The DEFINE command defines a macro that can later be called any number
-of times within a syntax file.  Each time it is called, the macro's
-body is @dfn{expanded}, that is, substituted, as if the body had been
-written instead of the macro call.  A macro may accept arguments,
-whose values are specified at the point of invocation and expanded in
-the body where they are referenced.  Macro bodies may also use various
-directives and functions, which are also expanded when the macro is
-called.
+@subsection Introduction
 
-Many identifiers associated with macros begin with @samp{!}, a
-character not normally allowed in identifiers.  These identifiers are
-reserved only for use with macros, which helps keep them from being
-confused with other kinds of identifiers.
+The DEFINE command creates a @dfn{macro}, which is a name for a
+fragment of PSPP syntax called the macro's @dfn{body}.  Following the
+DEFINE command, syntax may @dfn{call} the macro by name any number of
+times.  Each call substitutes, or @dfn{expands}, the macro's body in
+place of the call, as if the body had been written in its place.
 
-@node Macro Basics
-@subsection Macro Basics
-
-The simplest macros have no arguments.  The following defines a macro
-named @code{!vars} that expands to the variable names @code{v1 v2 v3},
-along with a few example uses.  The macro's name begins with @samp{!},
-which is optional for macro names.  The @code{()} following the macro
-name are required:
+The following syntax defines a macro named @code{!vars} that expands
+to the variable names @code{v1 v2 v3}.  The macro's name begins with
+@samp{!}, which is optional for macro names.  The @code{()} following
+the macro name are required:
 
 @example
 DEFINE !vars()
 v1 v2 v3
 !ENDDEFINE.
+@end example
 
+Here are two ways that @code{!vars} might be called given the
+preceding definition:
+
+@example
 DESCRIPTIVES !vars.
 FREQUENCIES /VARIABLES=!vars.
 @end example
 
-Macros can also expand to entire commands.  For example, the following
-example performs the same analyses as the last one:
+With macro expansion, the above calls are equivalent to the following:
 
 @example
-DEFINE !commands()
 DESCRIPTIVES v1 v2 v3.
 FREQUENCIES /VARIABLES=v1 v2 v3.
-!ENDDEFINE.
-
-!commands
 @end example
 
-The body of a macro can call another macro.  For example, we could
-combine the two preceding examples, with @code{!commands} calling
-@code{!vars} to obtain the variables to analyze.  The following shows
-one way that could work:
+The @code{!vars} macro expands to a fixed body.  Macros may have more
+sophisticated contents:
+
+@itemize @bullet
+@item
+Macro @dfn{arguments} that are substituted into the body whenever they
+are named.  The values of a macro's arguments are specified each time
+it is called.  @xref{Macro Arguments}.
+
+@item
+Macro @dfn{functions}, expanded when the macro is called.  @xref{Macro
+Functions}.
+
+@item
+@code{!IF} constructs, for conditional expansion.  @xref{Macro
+Conditional Expansion}.
+
+@item
+Two forms of @code{!DO} construct, for looping over a numerical range
+or a collection of tokens.  @xref{Macro Loops}.
+
+@item
+@code{!LET} constructs, for assigning to macro variables.  @xref{Macro
+Variable Assignment}.
+@end itemize
+
+Many identifiers associated with macros begin with @samp{!}, a
+character not normally allowed in identifiers.  These identifiers are
+reserved only for use with macros, which helps keep them from being
+confused with other kinds of identifiers.
+
+The following sections provide more details on macro syntax and
+semantics.
+
+@node Macro Bodies
+@subsection Macro Bodies
+
+As previously shown, a macro body may contain a fragment of a PSPP
+command (such as a variable name).  A macro body may also contain full
+PSPP commands.  In the latter case, the macro body should also contain
+the command terminators.
+
+Most PSPP commands may occur within a macro.  The @code{DEFINE}
+command itself is one exception, because the inner @code{!ENDDEFINE}
+ends the outer macro definition.  For compatibility, @code{BEGIN
+DATA}@dots{}@code{END DATA.} should not be used within a macro.
+
+The body of a macro may call another macro.  The following shows one
+way that could work:
 
 @example
 DEFINE !commands()
@@ -155,10 +203,39 @@ The following section shows how to do that.
 @node Macro Arguments
 @subsection Macro Arguments
 
-Macros may take any number of arguments, which are specified within
-the parentheses in the DEFINE command.  Arguments come in two
-varieties based on how their values are specified when the macro is
-called:
+This section explains how to use macro arguments.  As an initial
+example, the following syntax defines a macro named @code{!analyze}
+that takes all the syntax up to the first command terminator as an
+argument:
+
+@example
+DEFINE !analyze(!POSITIONAL !CMDEND)
+DESCRIPTIVES !1.
+FREQUENCIES /VARIABLES=!1.
+!ENDDEFINE.
+@end example
+
+@noindent When @code{!analyze} is called, it expands to a pair of analysis
+commands with each @code{!1} in the body replaced by the argument.
+That is, these calls:
+
+@example
+!analyze v1 v2 v3.
+!analyze v4 v5.
+@end example
+
+@noindent act like the following:
+
+@example
+DESCRIPTIVES v1 v2 v3.
+FREQUENCIES /VARIABLES=v1 v2 v3.
+DESCRIPTIVES v4 v5.
+FREQUENCIES /VARIABLES=v4 v5.
+@end example
+
+Macros may take any number of arguments, described within the
+parentheses in the DEFINE command.  Arguments come in two varieties
+based on how their values are specified when the macro is called:
 
 @itemize @bullet
 @item
@@ -166,10 +243,13 @@ A @dfn{positional} argument has a required value that follows the
 macro's name.  Use the @code{!POSITIONAL} keyword to declare a
 positional argument.
 
+When a macro is called, every positional argument must be given a
+value in the same order as the defintion.
+
 References to a positional argument in a macro body are numbered:
 @code{!1} is the first positional argument, @code{!2} the second, and
 so on.  In addition, @code{!*} expands to all of the positional
-argument values, separated by a space.
+arguments' values, separated by spaces.
 
 The following example uses a positional argument:
 
@@ -185,10 +265,10 @@ FREQUENCIES /VARIABLES=!1.
 
 @item
 A @dfn{keyword} argument has a name.  In the macro call, its value is
-specified with the syntax @code{@var{name}=@var{value}}.  Because of
-the names, keyword argument values may take any order in a macro call.
-If one is omitted, then a default value is used: either the value
-specified in @code{!DEFAULT(@var{value})}, or an empty value
+specified with the syntax @code{@i{name}=@i{value}}.  The names allow
+keyword argument values to take any order in the call, and even to be
+omitted.  When one is omitted, a default value is used: either the
+value specified in @code{!DEFAULT(@i{value})}, or an empty value
 otherwise.
 
 In declaration and calls, a keyword argument's name may not begin with
@@ -205,7 +285,7 @@ FREQUENCIES /VARIABLES=!vars.
 !ENDDEFINE.
 
 !analyze_kw vars=v1 v2 v3.  /* Analyze specified variables.
-!analyze_kw.                /* Analyze all variables.  
+!analyze_kw.                /* Analyze all variables.
 @end example
 @end itemize
 
@@ -216,7 +296,7 @@ values also come first in macro calls.
 Each argument declaration specifies the form of its value:
 
 @table @code
-@item !TOKENS(@var{count})
+@item !TOKENS(@i{count})
 Exactly @var{count} tokens, e.g.@: @code{!TOKENS(1)} for a single
 token.  Each identifier, number, quoted string, operator, or
 punctuator is a token.  @xref{Tokens}, for a complete definition.
@@ -291,7 +371,7 @@ FREQUENCIES /VARIABLES=!vars.
 By default, when an argument's value contains a macro call, the call
 is expanded each time the argument appears in the macro's body.  The
 @code{!NOEXPAND} keyword in an argument declaration suppresses this
-expansion.  @xref{Controlling Macro Expansion}, for details.
+expansion.  @xref{Controlling Macro Expansion}.
 
 @node Controlling Macro Expansion
 @subsection Controlling Macro Expansion
@@ -306,20 +386,20 @@ If a macro body contains @code{!OFFEXPAND} or @code{!ONEXPAND}
 directives, then @code{!OFFEXPAND} disables expansion of macro calls
 until the following @code{!ONEXPAND}.
 
-A macro argument's value may contain a macro call.  By default, these
-macro calls are expanded.  If the argument was declared with the
-@code{!NOEXPAND} keyword, they are not expanded.
+A macro argument's value may contain a macro call.  These macro calls
+are expanded, unless the argument was declared with the
+@code{!NOEXPAND} keyword.
 
 The argument to a macro function is a special context that does not
 expand macro calls.  For example, if @code{!vars} is the name of a
 macro, then @code{!LENGTH(!vars)} expands to 5, as does
 @code{!LENGTH(!1)} if positional argument 1 has value @code{!vars}.
-In these cases, use the @code{!EVAL} macro function to expand macros,
+To expand macros in these cases, use the @code{!EVAL} macro function,
 e.g.@: @code{!LENGTH(!EVAL(!vars))} or @code{!LENGTH(!EVAL(!1))}.
 @xref{Macro Functions}, for details.
 
-These rules apply to macro calls.  Uses of macro functions and macro
-arguments within a macro body are always expanded.
+These rules apply to macro calls, not to uses of macro functions and
+macro arguments within a macro body, which are always expanded.
 
 @node Macro Functions
 @subsection Macro Functions
@@ -330,9 +410,15 @@ characters.
 
 The arguments to macro functions have a restricted form.  They may
 only be a single token (such as an identifier or a string), a macro
-argument, or a call to a macro function.  Thus, @code{x}, @code{5.0},
-@code{x}, @code{!1}, @code{"5 + 6"}, and @code{!CONCAT(x,y)} are valid
-macro arguments, but @code{x y} and @code{5 + 6} are not.
+argument, or a call to a macro function.  Thus, the following are
+valid macro arguments:
+@example
+x    5.0    x    !1    "5 + 6"    !CONCAT(x,y)
+@end example
+@noindent and the following are not:
+@example
+x y    5+6
+@end example
 
 Macro functions expand to sequences of characters.  When these
 character strings are processed further as character strings, e.g.@:
@@ -409,7 +495,8 @@ than a single one.  For example:
 @deffn {Macro Function} !EVAL (arg)
 Expands macro calls in @var{arg}.  This is especially useful if
 @var{arg} is the name of a macro or a macro argument that expands to
-one, because arguments to macro functions are not expanded by default.
+one, because arguments to macro functions are not expanded by default
+(@pxref{Controlling Macro Expansion}).
 
 The following examples assume that @code{!vars} is a macro that
 expands to @code{a b c}:
@@ -621,6 +708,11 @@ body} multiple times, each time setting a named @dfn{loop variable} to
 a different value.  The loop body typically expands the loop variable
 at least once.
 
+The MITERATE setting (@pxref{SET MITERATE}) limits the number of
+iterations in a loop.  This is a safety measure to ensure that macro
+expansion terminates.  PSPP issues a warning when the MITERATE limit
+is exceeded.
+
 @subsubheading Loops Over Ranges
 
 @example
@@ -694,7 +786,8 @@ calls, that is, the nesting level of macro expansion.  The default is
 50.  This is mainly useful to avoid infinite expansion in the case of
 a macro that calls itself.
 
-MITERATE
+MITERATE (@pxref{SET MITERATE}) limits the number of iterations in a
+@code{!DO} construct.  The default is 1000.
 
 PRESERVE...RESTORE
 
@@ -703,13 +796,12 @@ SET MEXPAND, etc. doesn't work inside macro bodies.
 @node Macro Notes
 @subsection Extra Notes
 
-@code{!*} expands to all the positional arguments.
-
 Macros in comments.
 
 Macros in titles.
 
 Define ``unquote.''
+
 @node DO IF
 @section DO IF
 @vindex DO IF
index c21c66a73cab91b9eed75dfd1713159317561857..e91505c726ee271a84891c601e8d453f183a41e7 100644 (file)
@@ -113,9 +113,23 @@ cmd_define (struct lexer *lexer, struct dataset *ds UNUSED)
         }
       else
         {
+          if (lex_token (lexer) == T_MACRO_ID)
+            {
+              lex_error (lexer, _("Keyword macro parameter must be named in "
+                                  "definition without \"!\" prefix."));
+              goto error;
+            }
           if (!lex_force_id (lexer))
             goto error;
 
+          if (is_macro_keyword (lex_tokss (lexer)))
+            {
+              lex_error (lexer, _("Cannot use macro keyword \"%s\" "
+                                  "as an argument name."),
+                         lex_tokcstr (lexer));
+              goto error;
+            }
+
           p->positional = false;
           p->name = xasprintf ("!%s", lex_tokcstr (lexer));
           lex_get (lexer);
index 6d7decb40faccf0e97e85a40ca04379885c068c4..2accd94df95b752fa68dd5c90acdb2bc9ae61a63 100644 (file)
@@ -32,6 +32,7 @@
 #include "libpspp/str.h"
 #include "libpspp/string-array.h"
 #include "libpspp/string-map.h"
+#include "libpspp/stringi-set.h"
 
 #include "gl/c-ctype.h"
 #include "gl/ftoastr.h"
@@ -59,6 +60,41 @@ macro_token_to_representation (struct macro_token *mt, struct string *s)
   ds_put_substring (s, mt->representation);
 }
 
+bool
+is_macro_keyword (struct substring s)
+{
+  static struct stringi_set keywords = STRINGI_SET_INITIALIZER (keywords);
+  if (stringi_set_is_empty (&keywords))
+    {
+      static const char *kws[] = {
+        "BREAK",
+        "CHAREND",
+        "CMDEND",
+        "DEFAULT",
+        "DO",
+        "DOEND",
+        "ELSE",
+        "ENCLOSE",
+        "ENDDEFINE",
+        "IF",
+        "IFEND",
+        "IN",
+        "LET",
+        "NOEXPAND",
+        "OFFEXPAND",
+        "ONEXPAND",
+        "POSITIONAL",
+        "THEN",
+        "TOKENS",
+      };
+      for (size_t i = 0; i < sizeof kws / sizeof *kws; i++)
+        stringi_set_insert (&keywords, kws[i]);
+    }
+
+  ss_ltrim (&s, ss_cstr ("!"));
+  return stringi_set_contains_len (&keywords, s.string, s.length);
+}
+
 void
 macro_tokens_copy (struct macro_tokens *dst, const struct macro_tokens *src)
 {
@@ -543,8 +579,7 @@ me_enclose (struct macro_expander *me, const struct macro_token *mt)
 static const struct macro_param *
 macro_find_parameter_by_name (const struct macro *m, struct substring name)
 {
-  if (ss_first (name) == '!')
-    ss_advance (&name, 1);
+  ss_ltrim (&name, ss_cstr ("!"));
 
   for (size_t i = 0; i < m->n_params; i++)
     {
@@ -1478,6 +1513,12 @@ macro_parse_let (const struct macro_token *tokens, size_t n_tokens,
       return 0;
     }
   const struct substring var_name = p->token.string;
+  if (is_macro_keyword (var_name)
+      || macro_find_parameter_by_name (me->macro, var_name))
+    {
+      printf ("cannot use argument name or macro keyword as !LET variable\n");
+      return 0;
+    }
   p++;
 
   if (p >= end || p->token.type != T_EQUALS)
@@ -1538,8 +1579,15 @@ macro_expand_do (const struct macro_token *tokens, size_t n_tokens,
       return 0;
     }
   const struct substring var_name = p->token.string;
+  if (is_macro_keyword (var_name)
+      || macro_find_parameter_by_name (me->macro, var_name))
+    {
+      printf ("cannot use argument name or macro keyword as !DO variable\n");
+      return 0;
+    }
   p++;
 
+  int miterate = settings_get_miterate ();
   if (p < end && p->token.type == T_MACRO_ID
       && ss_equals_case (p->token.string, ss_cstr ("!IN")))
     {
@@ -1568,6 +1616,11 @@ macro_expand_do (const struct macro_token *tokens, size_t n_tokens,
       };
       for (size_t i = 0; i < items.n; i++)
         {
+          if (i >= miterate)
+            {
+              printf ("exceeded maximum number of iterations %d\n", miterate);
+              break;
+            }
           string_map_replace_nocopy (vars, ss_xstrdup (var_name),
                                      ss_xstrdup (items.mts[i].representation));
 
@@ -1625,21 +1678,31 @@ macro_expand_do (const struct macro_token *tokens, size_t n_tokens,
       };
 
       if ((by > 0 && first <= last) || (by < 0 && first >= last))
-        for (double index = first;
-             by > 0 ? (index <= last) : (index >= last);
-             index += by)
-          {
-            char index_s[DBL_BUFSIZE_BOUND];
-            c_dtoastr (index_s, sizeof index_s, 0, 0, index);
-            string_map_replace_nocopy (vars, ss_xstrdup (var_name),
-                                       xstrdup (index_s));
-
-            bool break_ = false;
-            macro_expand (&inner, nesting_countdown, macros,
-                          me, vars, expand, &break_, exp);
-            if (break_)
-              break;
-          }
+        {
+          int i = 0;
+          for (double index = first;
+               by > 0 ? (index <= last) : (index >= last);
+               index += by)
+            {
+              if (i++ > miterate)
+                {
+                  printf ("exceeded maximum number of iterations %d\n",
+                          miterate);
+                  break;
+                }
+
+              char index_s[DBL_BUFSIZE_BOUND];
+              c_dtoastr (index_s, sizeof index_s, 0, 0, index);
+              string_map_replace_nocopy (vars, ss_xstrdup (var_name),
+                                         xstrdup (index_s));
+
+              bool break_ = false;
+              macro_expand (&inner, nesting_countdown, macros,
+                            me, vars, expand, &break_, exp);
+              if (break_)
+                break;
+            }
+        }
 
       return do_end - tokens + 1;
     }
index 0b6fd0dac3f942bf244309152c698afd39710cb5..ac5cb65579a399570db80af3d779b75c8c825130 100644 (file)
@@ -38,6 +38,8 @@ void macro_token_uninit (struct macro_token *);
 
 void macro_token_to_representation (struct macro_token *, struct string *);
 
+bool is_macro_keyword (struct substring);
+
 struct macro_tokens
   {
     struct macro_token *mts;
index fa2670f9e6c367e67dace6389514e1c5ba709077..fa87f9a1de9f1c9859938f309b48cd4f1a3d7692 100644 (file)
@@ -275,6 +275,24 @@ note: unexpanded token "c"
 ])
 AT_CLEANUP
 
+AT_SETUP([keyword macro argument name with ! prefix])
+AT_DATA([define.sps], [dnl
+DEFINE !macro(!x=!TOKENS(1).
+])
+AT_CHECK([pspp -O format=csv define.sps], [1], [dnl
+"define.sps:1.15-1.16: error: DEFINE: Syntax error at `!x': Keyword macro parameter must be named in definition without ""!"" prefix."
+])
+AT_CLEANUP
+
+AT_SETUP([reserved macro keyword argument name])
+AT_DATA([define.sps], [dnl
+DEFINE !macro(if=!TOKENS(1).
+])
+AT_CHECK([pspp -O format=csv define.sps], [1], [dnl
+"define.sps:1.15-1.16: error: DEFINE: Syntax error at `if': Cannot use macro keyword ""if"" as an argument name."
+])
+AT_CLEANUP
+
 PSPP_CHECK_MACRO_EXPANSION([one !TOKENS(1) keyword argument],
   [DEFINE !k(arg1 = !TOKENS(1)) k(!arg1) !ENDDEFINE.],
   [!k arg1=x.
@@ -819,6 +837,96 @@ AT_CHECK([pspp --testing-mode define.sps], [0], [dnl
 ])
 AT_CLEANUP
 
+AT_SETUP([macro !DO invalid variable names])
+AT_KEYWORDS([index do])
+AT_DATA([define.sps], [dnl
+DEFINE !for(x=!TOKENS(1) / y=!TOKENS(1))
+!DO !x = !x !TO !y !var !DOEND.
+!ENDDEFINE.
+
+DEFINE !for2(x=!TOKENS(1) / y=!TOKENS(1))
+!DO !noexpand = !x !TO !y !var !DOEND.
+!ENDDEFINE.
+
+DEBUG EXPAND.
+!for x=1 y=5.
+!for2 x=1 y=5.
+])
+AT_CHECK([pspp --testing-mode define.sps], [0], [dnl
+cannot use argument name or macro keyword as !DO variable
+cannot use argument name or macro keyword as !DO variable
+!DO 1 = 1 !TO 5 !var !DOEND.
+
+!DO !noexpand = 1 !TO 5 !var !DOEND.
+])
+AT_CLEANUP
+
+AT_SETUP([macro indexed !DO reaches MITERATE])
+AT_KEYWORDS([index do])
+AT_DATA([define.sps], [dnl
+DEFINE !title(!POS !TOKENS(1)) !1. !ENDDEFINE.
+
+DEFINE !for(!POS !TOKENS(1) / !POS !TOKENS(1))
+!DO !var = !1 !TO !2 !var !DOEND.
+!ENDDEFINE.
+
+DEFINE !forby(!POS !TOKENS(1) / !POS !TOKENS(1) / !POS !TOKENS(1))
+!DO !var = !1 !TO !2 !BY !3 !var !DOEND.
+!ENDDEFINE.
+
+SET MITERATE=3.
+DEBUG EXPAND.
+!title "increasing".
+!for 1 5.
+!forby 1 5 1.
+!forby 1 5 2.
+!forby 1 5 2.5.
+!forby 1 5 -1.
+
+!title "decreasing".
+!for 5 1.
+!forby 5 1 1.
+!forby 5 1 -1.
+!forby 5 1 -2.
+!forby 5 1 -3.
+
+!title "non-integer".
+!for 1.5 3.5.
+])
+AT_CHECK([pspp --testing-mode define.sps], [0], [dnl
+exceeded maximum number of iterations 3
+exceeded maximum number of iterations 3
+exceeded maximum number of iterations 3
+"increasing".
+
+1 2 3 4.
+
+1 2 3 4.
+
+1 3 5.
+
+1 3.5.
+
+.
+
+"decreasing".
+
+.
+
+.
+
+5 4 3 2.
+
+5 3 1.
+
+5 2.
+
+"non-integer".
+
+1.5 2.5 3.5.
+])
+AT_CLEANUP
+
 AT_SETUP([!BREAK with macro indexed !DO])
 AT_KEYWORDS([index do break])
 AT_DATA([define.sps], [dnl
@@ -867,6 +975,30 @@ AT_CHECK([pspp --testing-mode define.sps], [0], [dnl
 ])
 AT_CLEANUP
 
+AT_SETUP([macro list !DO reaches MITERATE])
+AT_KEYWORDS([index do])
+AT_DATA([define.sps], [dnl
+DEFINE !for(!POS !CMDEND)
+(!DO !i !IN (!1) (!i) !DOEND).
+!ENDDEFINE.
+
+SET MITERATE=2.
+DEBUG EXPAND.
+!for a b c.
+!for 'foo bar baz quux'.
+!for.
+])
+AT_CHECK([pspp --testing-mode define.sps], [0], [dnl
+exceeded maximum number of iterations 2
+exceeded maximum number of iterations 2
+( (a) (b) ).
+
+( (foo) (bar) ).
+
+( ).
+])
+AT_CLEANUP
+
 AT_SETUP([!BREAK with macro list !DO])
 AT_KEYWORDS([index break do])
 AT_DATA([define.sps], [dnl
@@ -897,3 +1029,87 @@ AT_CHECK([pspp --testing-mode define.sps], [0], [dnl
 ( ).
 ])
 AT_CLEANUP
+
+AT_SETUP([macro !LET])
+AT_DATA([define.sps], [dnl
+DEFINE !macro(!pos !enclose('(',')'))
+!LET !x=!1
+!LET !y=!QUOTE(!1)
+!LET !z=(!y="abc")
+!y !z
+!ENDDEFINE.
+
+DEBUG EXPAND.
+!macro(1+2).
+!macro(abc).
+])
+AT_CHECK([pspp --testing-mode define.sps -O format=csv], [0], [dnl
+1 + 2 0
+
+abc 1
+])
+AT_CLEANUP
+
+AT_SETUP([macro !LET invalid variable names])
+AT_DATA([define.sps], [dnl
+DEFINE !macro(x=!tokens(1))
+!LET !x=!x
+!ENDDEFINE.
+
+DEFINE !macro2()
+!LET !do=x
+!ENDDEFINE.
+
+DEBUG EXPAND.
+!macro 1.
+!macro2.
+])
+AT_CHECK([pspp --testing-mode define.sps -O format=csv], [0], [dnl
+cannot use argument name or macro keyword as !LET variable
+cannot use argument name or macro keyword as !LET variable
+expected macro variable name following !DO
+!LET =
+
+!LET !do = x
+])
+AT_CLEANUP
+
+AT_SETUP([BEGIN DATA inside a macro])
+AT_DATA([define.sps], [dnl
+DEFINE !macro()
+DATA LIST NOTABLE /x 1.
+BEGIN DATA
+1
+2
+3
+END DATA.
+LIST.
+!ENDDEFINE.
+
+!macro
+])
+AT_CHECK([pspp define.sps -O format=csv], [0], [dnl
+Table: Data List
+x
+1
+2
+3
+])
+AT_CLEANUP
+
+AT_SETUP([TITLE and SUBTITLE with macros])
+AT_KEYWORDS([macro])
+for command in TITLE SUBTITLE; do
+    cat >title.sps <<EOF
+DEFINE !paste(!POS !TOKENS(1) / !POS !TOKENS(1))
+!CONCAT(!1,!2)
+!ENDDEFINE.
+$command prefix !paste foo bar.
+SHOW $command.
+EOF
+    cat >expout <<EOF
+title.sps:2: note: SHOW: $command is foo  bar.
+EOF
+    AT_CHECK([pspp -O format=csv title.sps], [0], [expout])
+done
+AT_CLEANUP
\ No newline at end of file