improve macro error messages
[pspp] / src / language / lexer / macro.c
index e900a5e7d1a542f0ec1faedaa813b0bd9ac883dc..bee23c999cb1be7f6254d69cdf315eea6206319c 100644 (file)
 #include "language/lexer/segment.h"
 #include "language/lexer/scan.h"
 #include "libpspp/assertion.h"
+#include "libpspp/cast.h"
 #include "libpspp/i18n.h"
 #include "libpspp/message.h"
 #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"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
@@ -54,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)
 {
@@ -99,7 +140,7 @@ macro_tokens_from_string (struct macro_tokens *mts, const struct substring src,
     };
 
   struct state state = {
-    .segmenter = SEGMENTER_INIT (mode),
+    .segmenter = segmenter_init (mode, true),
     .body = src,
   };
   struct state saved = state;
@@ -246,28 +287,37 @@ classify_token (enum token_type type)
 }
 
 void
-macro_tokens_to_representation (struct macro_tokens *mts, struct string *s)
+macro_tokens_to_representation (struct macro_tokens *mts, struct string *s,
+                                size_t *ofs, size_t *len)
 {
+  assert ((ofs != NULL) == (len != NULL));
+
   if (!mts->n)
     return;
 
-  macro_token_to_representation (&mts->mts[0], s);
-  for (size_t i = 1; i < mts->n; i++)
+  for (size_t i = 0; i < mts->n; i++)
     {
-      enum token_type prev = mts->mts[i - 1].token.type;
-      enum token_type next = mts->mts[i].token.type;
-
-      if (prev == T_ENDCMD)
-        ds_put_byte (s, '\n');
-      else
+      if (i > 0)
         {
-          enum token_class pc = classify_token (prev);
-          enum token_class nc = classify_token (next);
-          if (needs_space (pc, nc))
-            ds_put_byte (s, ' ');
+          enum token_type prev = mts->mts[i - 1].token.type;
+          enum token_type next = mts->mts[i].token.type;
+
+          if (prev == T_ENDCMD)
+            ds_put_byte (s, '\n');
+          else
+            {
+              enum token_class pc = classify_token (prev);
+              enum token_class nc = classify_token (next);
+              if (needs_space (pc, nc))
+                ds_put_byte (s, ' ');
+            }
         }
 
+      if (ofs)
+        ofs[i] = s->ss.length;
       macro_token_to_representation (&mts->mts[i], s);
+      if (len)
+        len[i] = s->ss.length - ofs[i];
     }
 }
 
@@ -538,8 +588,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++)
     {
@@ -704,28 +753,41 @@ macro_expander_add (struct macro_expander *me, const struct macro_token *mt)
 */
 struct parse_macro_function_ctx
   {
-    struct macro_token *input;
+    const struct macro_token *input;
     size_t n_input;
     int nesting_countdown;
     const struct macro_set *macros;
     const struct macro_expander *me;
+    struct string_map *vars;
     bool *expand;
   };
 
 static void
 macro_expand (const struct macro_tokens *,
               int nesting_countdown, const struct macro_set *,
-              const struct macro_expander *, bool *expand, struct macro_tokens *exp);
+              const struct macro_expander *, struct string_map *vars,
+              bool *expand, bool *break_, struct macro_tokens *exp);
 
 static bool
 expand_macro_function (struct parse_macro_function_ctx *ctx,
                        struct string *output, size_t *input_consumed);
 
+/* Returns true if the pair of tokens starting at offset OFS within MTS are !*,
+   false otherwise. */
+static bool
+is_bang_star (const struct macro_token *mts, size_t n, size_t ofs)
+{
+  return (ofs + 1 < n
+          && mts[ofs].token.type == T_MACRO_ID
+          && ss_equals (mts[ofs].token.string, ss_cstr ("!"))
+          && mts[ofs + 1].token.type == T_ASTERISK);
+}
+
 static size_t
 parse_function_arg (struct parse_macro_function_ctx *ctx,
                     size_t i, struct string *farg)
 {
-  struct macro_token *tokens = ctx->input;
+  const struct macro_token *tokens = ctx->input;
   const struct token *token = &tokens[i].token;
   if (token->type == T_MACRO_ID)
     {
@@ -744,12 +806,43 @@ parse_function_arg (struct parse_macro_function_ctx *ctx,
           return 1;
         }
 
+      if (is_bang_star (ctx->input, ctx->n_input, i))
+        {
+          for (size_t i = 0; i < ctx->me->macro->n_params; i++)
+            {
+              if (!ctx->me->macro->params[i].positional)
+                break;
+
+              const struct macro_tokens *marg = ctx->me->args[i];
+              for (size_t j = 0; j < marg->n; j++)
+                {
+                  if (i || j)
+                    ds_put_byte (farg, ' ');
+                  ds_put_substring (farg, marg->mts[j].representation);
+                }
+            }
+          return 2;
+        }
+
+      if (ctx->vars)
+        {
+          const char *value = string_map_find__ (ctx->vars,
+                                                 token->string.string,
+                                                 token->string.length);
+          if (value)
+            {
+              ds_put_cstr (farg, value);
+              return 1;
+            }
+        }
+
       struct parse_macro_function_ctx subctx = {
         .input = &ctx->input[i],
         .n_input = ctx->n_input - i,
         .nesting_countdown = ctx->nesting_countdown,
         .macros = ctx->macros,
         .me = ctx->me,
+        .vars = ctx->vars,
         .expand = ctx->expand,
       };
       size_t subinput_consumed;
@@ -768,12 +861,12 @@ parse_macro_function (struct parse_macro_function_ctx *ctx,
                       int min_args, int max_args,
                       size_t *input_consumed)
 {
-  struct macro_token *tokens = ctx->input;
+  const struct macro_token *tokens = ctx->input;
   size_t n_tokens = ctx->n_input;
 
   if (!n_tokens
       || tokens[0].token.type != T_MACRO_ID
-      || !ss_equals_case (tokens[0].token.string, function))
+      || !ss_equals_case (tokens[0].token.string, function)) /* XXX abbrevs allowed */
     return false;
 
   if (n_tokens < 2 || tokens[1].token.type != T_LPAREN)
@@ -830,7 +923,8 @@ static bool
 unquote_string (const char *s, struct string *content)
 {
   struct string_lexer slex;
-  string_lexer_init (&slex, s, strlen (s), SEG_MODE_INTERACTIVE /* XXX */);
+  string_lexer_init (&slex, s, strlen (s), SEG_MODE_INTERACTIVE /* XXX */,
+                     true);
 
   struct token token1;
   if (!string_lexer_next (&slex, &token1))
@@ -975,7 +1069,7 @@ expand_macro_function (struct parse_macro_function_ctx *ctx,
       if (mts.n > 1)
         {
           struct macro_tokens tail = { .mts = mts.mts + 1, .n = mts.n - 1 };
-          macro_tokens_to_representation (&tail, output);
+          macro_tokens_to_representation (&tail, output, NULL, NULL);
         }
       macro_tokens_uninit (&mts);
       ds_destroy (&tmp);
@@ -1003,9 +1097,9 @@ expand_macro_function (struct parse_macro_function_ctx *ctx,
       macro_tokens_from_string (&mts, ss_cstr (args.strings[0]),
                                 SEG_MODE_INTERACTIVE /* XXX */);
       struct macro_tokens exp = { .n = 0 };
-      macro_expand (&mts, ctx->nesting_countdown - 1, ctx->macros,
-                    ctx->me, ctx->expand, &exp);
-      macro_tokens_to_representation (&exp, output);
+      macro_expand (&mts, ctx->nesting_countdown - 1, ctx->macros, ctx->me,
+                    ctx->vars, ctx->expand, NULL, &exp);
+      macro_tokens_to_representation (&exp, output, NULL, NULL);
       macro_tokens_uninit (&exp);
       macro_tokens_uninit (&mts);
     }
@@ -1023,11 +1117,616 @@ expand_macro_function (struct parse_macro_function_ctx *ctx,
   return true;
 }
 
+struct expr_context
+  {
+    int nesting_countdown;
+    const struct macro_set *macros;
+    const struct macro_expander *me;
+    struct string_map *vars;
+    bool *expand;
+  };
+
+static char *macro_evaluate_or (const struct expr_context *ctx,
+                                const struct macro_token **tokens,
+                                const struct macro_token *end);
+
+static char *
+macro_evaluate_literal (const struct expr_context *ctx,
+                        const struct macro_token **tokens,
+                        const struct macro_token *end)
+{
+  const struct macro_token *p = *tokens;
+  if (p >= end)
+    return NULL;
+  if (p->token.type == T_LPAREN)
+    {
+      p++;
+      char *value = macro_evaluate_or (ctx, &p, end);
+      if (!value)
+        return NULL;
+      if (p >= end || p->token.type != T_RPAREN)
+        {
+          free (value);
+          printf ("expecting ')' in macro expression\n");
+          return NULL;
+        }
+      p++;
+      *tokens = p;
+      return value;
+    }
+
+  struct parse_macro_function_ctx fctx = {
+    .input = p,
+    .n_input = end - p,
+    .nesting_countdown = ctx->nesting_countdown,
+    .macros = ctx->macros,
+    .me = ctx->me,
+    .vars = ctx->vars,
+    .expand = ctx->expand,
+  };
+  struct string function_output = DS_EMPTY_INITIALIZER;
+  size_t function_consumed = parse_function_arg (&fctx, 0, &function_output);
+  struct string unquoted = DS_EMPTY_INITIALIZER;
+  if (unquote_string (ds_cstr (&function_output), &unquoted))
+    {
+      ds_swap (&function_output, &unquoted);
+      ds_destroy (&unquoted);
+    }
+  *tokens = p + function_consumed;
+  return ds_steal_cstr (&function_output);
+}
+
+/* Returns true if MT is valid as a macro operator.  Only operators written as
+   symbols (e.g. <>) are usable in macro expressions, not operator written as
+   letters (e.g. EQ). */
+static bool
+is_macro_operator (const struct macro_token *mt)
+{
+  return (mt->representation.length > 0
+          && !c_isalpha (mt->representation.string[0]));
+}
+
+static enum token_type
+parse_relational_op (const struct macro_token *mt)
+{
+  switch (mt->token.type)
+    {
+    case T_EQUALS:
+      return T_EQ;
+
+    case T_NE:
+    case T_LT:
+    case T_GT:
+    case T_LE:
+    case T_GE:
+      return is_macro_operator (mt) ? mt->token.type : T_STOP;
+
+    case T_MACRO_ID:
+      return (ss_equals_case (mt->token.string, ss_cstr ("!EQ")) ? T_EQ
+              : ss_equals_case (mt->token.string, ss_cstr ("!NE")) ? T_NE
+              : ss_equals_case (mt->token.string, ss_cstr ("!LT")) ? T_LT
+              : ss_equals_case (mt->token.string, ss_cstr ("!GT")) ? T_GT
+              : ss_equals_case (mt->token.string, ss_cstr ("!LE")) ? T_LE
+              : ss_equals_case (mt->token.string, ss_cstr ("!GE")) ? T_GE
+              : T_STOP);
+
+    default:
+      return T_STOP;
+    }
+}
+
+static char *
+macro_evaluate_relational (const struct expr_context *ctx,
+                           const struct macro_token **tokens,
+                           const struct macro_token *end)
+{
+  const struct macro_token *p = *tokens;
+  char *lhs = macro_evaluate_literal (ctx, &p, end);
+  if (!lhs)
+    return NULL;
+
+  enum token_type op = p >= end ? T_STOP : parse_relational_op (p);
+  if (op == T_STOP)
+    {
+      *tokens = p;
+      return lhs;
+    }
+  p++;
+
+  char *rhs = macro_evaluate_literal (ctx, &p, end);
+  if (!rhs)
+    {
+      free (lhs);
+      return NULL;
+    }
+
+  struct string lhs_tmp, rhs_tmp;
+  int cmp = strcmp/*XXX*/ (unquote_string_in_place (lhs, &lhs_tmp),
+                           unquote_string_in_place (rhs, &rhs_tmp));
+  ds_destroy (&lhs_tmp);
+  ds_destroy (&rhs_tmp);
+
+  free (lhs);
+  free (rhs);
+
+  bool b = (op == T_EQUALS || op == T_EQ ? !cmp
+            : op == T_NE ? cmp
+            : op == T_LT ? cmp < 0
+            : op == T_GT ? cmp > 0
+            : op == T_LE ? cmp <= 0
+            :/*op == T_GE*/cmp >= 0);
+
+  *tokens = p;
+  return xstrdup (b ? "1" : "0");
+}
+
+static char *
+macro_evaluate_not (const struct expr_context *ctx,
+                    const struct macro_token **tokens,
+                    const struct macro_token *end)
+{
+  const struct macro_token *p = *tokens;
+
+  unsigned int negations = 0;
+  while (p < end
+         && (ss_equals_case (p->representation, ss_cstr ("!NOT"))
+             || ss_equals (p->representation, ss_cstr ("~"))))
+    {
+      p++;
+      negations++;
+    }
+
+  char *operand = macro_evaluate_relational (ctx, &p, end);
+  if (!operand || !negations)
+    {
+      *tokens = p;
+      return operand;
+    }
+
+  bool b = strcmp (operand, "0") ^ (negations & 1);
+  free (operand);
+  *tokens = p;
+  return xstrdup (b ? "1" : "0");
+}
+
+static char *
+macro_evaluate_and (const struct expr_context *ctx,
+                    const struct macro_token **tokens,
+                    const struct macro_token *end)
+{
+  const struct macro_token *p = *tokens;
+  char *lhs = macro_evaluate_not (ctx, &p, end);
+  if (!lhs)
+    return NULL;
+
+  while (p < end
+         && (ss_equals_case (p->representation, ss_cstr ("!AND"))
+             || ss_equals (p->representation, ss_cstr ("&"))))
+    {
+      p++;
+      char *rhs = macro_evaluate_not (ctx, &p, end);
+      if (!rhs)
+        {
+          free (lhs);
+          return NULL;
+        }
+
+      bool b = strcmp (lhs, "0") && strcmp (rhs, "0");
+      free (lhs);
+      free (rhs);
+      lhs = xstrdup (b ? "1" : "0");
+    }
+  *tokens = p;
+  return lhs;
+}
+
+static char *
+macro_evaluate_or (const struct expr_context *ctx,
+                   const struct macro_token **tokens,
+                   const struct macro_token *end)
+{
+  const struct macro_token *p = *tokens;
+  char *lhs = macro_evaluate_and (ctx, &p, end);
+  if (!lhs)
+    return NULL;
+
+  while (p < end
+         && (ss_equals_case (p->representation, ss_cstr ("!OR"))
+             || ss_equals (p->representation, ss_cstr ("|"))))
+    {
+      p++;
+      char *rhs = macro_evaluate_and (ctx, &p, end);
+      if (!rhs)
+        {
+          free (lhs);
+          return NULL;
+        }
+
+      bool b = strcmp (lhs, "0") || strcmp (rhs, "0");
+      free (lhs);
+      free (rhs);
+      lhs = xstrdup (b ? "1" : "0");
+    }
+  *tokens = p;
+  return lhs;
+}
+
+static char *
+macro_evaluate_expression (const struct macro_token **tokens, size_t n_tokens,
+                           int nesting_countdown, const struct macro_set *macros,
+                           const struct macro_expander *me, struct string_map *vars,
+                           bool *expand)
+{
+  const struct expr_context ctx = {
+    .nesting_countdown = nesting_countdown,
+    .macros = macros,
+    .me = me,
+    .vars = vars,
+    .expand = expand,
+  };
+  return macro_evaluate_or (&ctx, tokens, *tokens + n_tokens);
+}
+
+static bool
+macro_evaluate_number (const struct macro_token **tokens, size_t n_tokens,
+                       int nesting_countdown, const struct macro_set *macros,
+                       const struct macro_expander *me, struct string_map *vars,
+                       bool *expand, double *number)
+{
+  char *s = macro_evaluate_expression (tokens, n_tokens, nesting_countdown,
+                                       macros, me, vars, expand);
+  if (!s)
+    return false;
+
+  struct macro_tokens mts = { .n = 0 };
+  macro_tokens_from_string (&mts, ss_cstr (s), SEG_MODE_INTERACTIVE /* XXX */);
+  if (mts.n != 1 || !token_is_number (&mts.mts[0].token))
+    {
+      macro_tokens_print (&mts, stdout);
+      printf ("expression must evaluate to a number (not %s)\n", s);
+      free (s);
+      macro_tokens_uninit (&mts);
+      return false;
+    }
+
+  *number = token_number (&mts.mts[0].token);
+  free (s);
+  macro_tokens_uninit (&mts);
+  return true;
+}
+
+static const struct macro_token *
+find_ifend_clause (const struct macro_token *p, const struct macro_token *end)
+{
+  size_t nesting = 0;
+  for (; p < end; p++)
+    {
+      if (p->token.type != T_MACRO_ID)
+        continue;
+
+      if (ss_equals_case (p->token.string, ss_cstr ("!IF")))
+        nesting++;
+      else if (ss_equals_case (p->token.string, ss_cstr ("!IFEND")))
+        {
+          if (!nesting)
+            return p;
+          nesting--;
+        }
+      else if (ss_equals_case (p->token.string, ss_cstr ("!ELSE")) && !nesting)
+        return p;
+    }
+  return NULL;
+}
+
+static size_t
+macro_expand_if (const struct macro_token *tokens, size_t n_tokens,
+                 int nesting_countdown, const struct macro_set *macros,
+                 const struct macro_expander *me, struct string_map *vars,
+                 bool *expand, bool *break_, struct macro_tokens *exp)
+{
+  const struct macro_token *p = tokens;
+  const struct macro_token *end = tokens + n_tokens;
+
+  if (p >= end || !ss_equals_case (p->token.string, ss_cstr ("!IF")))
+    return 0;
+
+  p++;
+  char *result = macro_evaluate_expression (&p, end - p,
+                                            nesting_countdown, macros, me, vars,
+                                            expand);
+  if (!result)
+    return 0;
+  bool b = strcmp (result, "0");
+  free (result);
+
+  if (p >= end
+      || p->token.type != T_MACRO_ID
+      || !ss_equals_case (p->token.string, ss_cstr ("!THEN")))
+    {
+      printf ("!THEN expected\n");
+      return 0;
+    }
+
+  const struct macro_token *start_then = p + 1;
+  const struct macro_token *end_then = find_ifend_clause (start_then, end);
+  if (!end_then)
+    {
+      printf ("!ELSE or !IFEND expected\n");
+      return 0;
+    }
+
+  const struct macro_token *start_else, *end_if;
+  if (ss_equals_case (end_then->token.string, ss_cstr ("!ELSE")))
+    {
+      start_else = end_then + 1;
+      end_if = find_ifend_clause (start_else, end);
+      if (!end_if
+          || !ss_equals_case (end_if->token.string, ss_cstr ("!IFEND")))
+        {
+          printf ("!IFEND expected\n");
+          return 0;
+        }
+    }
+  else
+    {
+      start_else = NULL;
+      end_if = end_then;
+    }
+
+  const struct macro_token *start;
+  size_t n;
+  if (b)
+    {
+      start = start_then;
+      n = end_then - start_then;
+    }
+  else if (start_else)
+    {
+      start = start_else;
+      n = end_if - start_else;
+    }
+  else
+    {
+      start = NULL;
+      n = 0;
+    }
+
+  if (n)
+    {
+      struct macro_tokens mts = {
+        .mts = CONST_CAST (struct macro_token *, start),
+        .n = n,
+      };
+      macro_expand (&mts, nesting_countdown, macros, me, vars, expand,
+                    break_, exp);
+    }
+  return (end_if + 1) - tokens;
+}
+
+static size_t
+macro_parse_let (const struct macro_token *tokens, size_t n_tokens,
+                 int nesting_countdown, const struct macro_set *macros,
+                 const struct macro_expander *me, struct string_map *vars,
+                 bool *expand)
+{
+  const struct macro_token *p = tokens;
+  const struct macro_token *end = tokens + n_tokens;
+
+  if (p >= end || !ss_equals_case (p->token.string, ss_cstr ("!LET")))
+    return 0;
+  p++;
+
+  if (p >= end || p->token.type != T_MACRO_ID)
+    {
+      printf ("expected macro variable name following !LET\n");
+      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)
+    {
+      printf ("expected = following !LET\n");
+      return 0;
+    }
+  p++;
+
+  char *value = macro_evaluate_expression (&p, end - p,
+                                           nesting_countdown, macros, me, vars,
+                                           expand);
+  if (!value)
+    return 0;
+
+  string_map_replace_nocopy (vars, ss_xstrdup (var_name), value);
+  return p - tokens;
+}
+
+static const struct macro_token *
+find_doend (const struct macro_token *p, const struct macro_token *end)
+{
+  size_t nesting = 0;
+  for (; p < end; p++)
+    {
+      if (p->token.type != T_MACRO_ID)
+        continue;
+
+      if (ss_equals_case (p->token.string, ss_cstr ("!DO")))
+        nesting++;
+      else if (ss_equals_case (p->token.string, ss_cstr ("!DOEND")))
+        {
+          if (!nesting)
+            return p;
+          nesting--;
+        }
+    }
+  printf ("missing !DOEND\n");
+  return NULL;
+}
+
+static size_t
+macro_expand_do (const struct macro_token *tokens, size_t n_tokens,
+                 int nesting_countdown, const struct macro_set *macros,
+                 const struct macro_expander *me, struct string_map *vars,
+                 bool *expand, struct macro_tokens *exp)
+{
+  const struct macro_token *p = tokens;
+  const struct macro_token *end = tokens + n_tokens;
+
+  if (p >= end || !ss_equals_case (p->token.string, ss_cstr ("!DO")))
+    return 0;
+  p++;
+
+  if (p >= end || p->token.type != T_MACRO_ID)
+    {
+      printf ("expected macro variable name following !DO\n");
+      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")))
+    {
+      p++;
+      char *list = macro_evaluate_expression (&p, end - p,
+                                              nesting_countdown, macros, me, vars,
+                                              expand);
+      if (!list)
+        return 0;
+
+      struct macro_tokens items = { .n = 0 };
+      macro_tokens_from_string (&items, ss_cstr (list),
+                                SEG_MODE_INTERACTIVE /* XXX */);
+      free (list);
+
+      const struct macro_token *do_end = find_doend (p, end);
+      if (!do_end)
+        {
+          macro_tokens_uninit (&items);
+          return 0;
+        }
+
+      const struct macro_tokens inner = {
+        .mts = CONST_CAST (struct macro_token *, p),
+        .n = do_end - p
+      };
+      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));
+
+          bool break_ = false;
+          macro_expand (&inner, nesting_countdown, macros,
+                        me, vars, expand, &break_, exp);
+          if (break_)
+            break;
+        }
+      return do_end - tokens + 1;
+    }
+  else if (p < end && p->token.type == T_EQUALS)
+    {
+      p++;
+      double first;
+      if (!macro_evaluate_number (&p, end - p, nesting_countdown, macros, me,
+                                  vars, expand, &first))
+        return 0;
+
+      if (p >= end || p->token.type != T_MACRO_ID
+          || !ss_equals_case (p->token.string, ss_cstr ("!TO")))
+        {
+          printf ("expecting !TO\n");
+          return 0;
+        }
+      p++;
+
+      double last;
+      if (!macro_evaluate_number (&p, end - p, nesting_countdown, macros, me,
+                                  vars, expand, &last))
+        return 0;
+
+      double by = 1.0;
+      if (p < end && p->token.type == T_MACRO_ID
+          && ss_equals_case (p->token.string, ss_cstr ("!BY")))
+        {
+          p++;
+          if (!macro_evaluate_number (&p, end - p, nesting_countdown, macros, me,
+                                      vars, expand, &by))
+            return 0;
+
+          if (by == 0.0)
+            {
+              printf ("!BY value cannot be zero\n");
+              return 0;
+            }
+        }
+
+      const struct macro_token *do_end = find_doend (p, end);
+      if (!do_end)
+        return 0;
+      const struct macro_tokens inner = {
+        .mts = CONST_CAST (struct macro_token *, p),
+        .n = do_end - p
+      };
+
+      if ((by > 0 && first <= last) || (by < 0 && first >= last))
+        {
+          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;
+    }
+  else
+    {
+      printf ("expecting = or !IN in !DO loop\n");
+      return 0;
+    }
+}
+
 static void
 macro_expand (const struct macro_tokens *mts,
               int nesting_countdown, const struct macro_set *macros,
-              const struct macro_expander *me, bool *expand,
-              struct macro_tokens *exp)
+              const struct macro_expander *me, struct string_map *vars,
+              bool *expand, bool *break_, struct macro_tokens *exp)
 {
   if (nesting_countdown <= 0)
     {
@@ -1037,7 +1736,11 @@ macro_expand (const struct macro_tokens *mts,
       return;
     }
 
-  for (size_t i = 0; i < mts->n; i++)
+  struct string_map own_vars = STRING_MAP_INITIALIZER (own_vars);
+  if (!vars)
+    vars = &own_vars;
+
+  for (size_t i = 0; i < mts->n && (!break_ || !*break_); i++)
     {
       const struct macro_token *mt = &mts->mts[i];
       const struct token *token = &mt->token;
@@ -1050,12 +1753,54 @@ macro_expand (const struct macro_tokens *mts,
               const struct macro_tokens *arg = me->args[param - me->macro->params];
               //macro_tokens_print (arg, stdout);
               if (*expand && param->expand_arg)
-                macro_expand (arg, nesting_countdown, macros, NULL, expand, exp);
+                macro_expand (arg, nesting_countdown, macros, NULL, NULL,
+                              expand, break_, exp);
               else
                 for (size_t i = 0; i < arg->n; i++)
                   macro_tokens_add (exp, &arg->mts[i]);
               continue;
             }
+
+          if (is_bang_star (mts->mts, mts->n, i))
+            {
+              for (size_t j = 0; j < me->macro->n_params; j++)
+                {
+                  const struct macro_param *param = &me->macro->params[j];
+                  if (!param->positional)
+                    break;
+
+                  const struct macro_tokens *arg = me->args[j];
+                  if (*expand && param->expand_arg)
+                    macro_expand (arg, nesting_countdown, macros, NULL, NULL,
+                                  expand, break_, exp);
+                  else
+                    for (size_t k = 0; k < arg->n; k++)
+                      macro_tokens_add (exp, &arg->mts[k]);
+                }
+              i++;
+              continue;
+            }
+
+          size_t n = macro_expand_if (&mts->mts[i], mts->n - i,
+                                      nesting_countdown, macros, me, vars,
+                                      expand, break_, exp);
+          if (n > 0)
+            {
+              i += n - 1;
+              continue;
+            }
+        }
+
+      if (token->type == T_MACRO_ID && vars)
+        {
+          const char *value = string_map_find__ (vars, token->string.string,
+                                                 token->string.length);
+          if (value)
+            {
+              macro_tokens_from_string (exp, ss_cstr (value),
+                                        SEG_MODE_INTERACTIVE /* XXX */);
+              continue;
+            }
         }
 
       if (*expand)
@@ -1072,7 +1817,7 @@ macro_expand (const struct macro_tokens *mts,
             {
               i += retval - 1;
               macro_expand (&subme->macro->body, nesting_countdown - 1, macros,
-                            subme, expand, exp);
+                            subme, NULL, expand, break_, exp);
               macro_expander_destroy (subme);
               continue;
             }
@@ -1086,14 +1831,24 @@ macro_expand (const struct macro_tokens *mts,
           continue;
         }
 
-      /* Maybe each arg should just be a string, either a quoted string or a
-         non-quoted string containing tokens. */
+      if (ss_equals_case (token->string, ss_cstr ("!break")))
+        {
+          if (!break_)
+            printf ("!BREAK outside !DO\n");
+          else
+            {
+              *break_ = true;
+              break;
+            }
+        }
+
       struct parse_macro_function_ctx ctx = {
         .input = &mts->mts[i],
         .n_input = mts->n - i,
         .nesting_countdown = nesting_countdown,
         .macros = macros,
         .me = me,
+        .vars = vars,
         .expand = expand,
       };
       struct string function_output = DS_EMPTY_INITIALIZER;
@@ -1109,6 +1864,24 @@ macro_expand (const struct macro_tokens *mts,
           continue;
         }
 
+      size_t n = macro_parse_let (&mts->mts[i], mts->n - i,
+                                  nesting_countdown, macros, me, vars,
+                                  expand);
+      if (n > 0)
+        {
+          i += n - 1;
+          continue;
+        }
+
+      n = macro_expand_do (&mts->mts[i], mts->n - i,
+                           nesting_countdown, macros, me, vars,
+                           expand, exp);
+      if (n > 0)
+        {
+          i += n - 1;
+          continue;
+        }
+
       if (ss_equals_case (token->string, ss_cstr ("!onexpand")))
         *expand = true;
       else if (ss_equals_case (token->string, ss_cstr ("!offexpand")))
@@ -1116,6 +1889,8 @@ macro_expand (const struct macro_tokens *mts,
       else
         macro_tokens_add (exp, mt);
     }
+  if (vars == &own_vars)
+    string_map_destroy (&own_vars);
 }
 
 void
@@ -1131,7 +1906,7 @@ macro_expander_get_expansion (struct macro_expander *me, struct macro_tokens *ex
 
   bool expand = true;
   macro_expand (&me->macro->body, settings_get_mnest (),
-                me->macros, me, &expand, exp);
+                me->macros, me, NULL, &expand, NULL, exp);
 
 #if 0
   printf ("expansion:\n");