macro conditional expressions (no tests yet)
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 13 Jun 2021 01:25:32 +0000 (18:25 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 13 Jun 2021 01:25:32 +0000 (18:25 -0700)
doc/flow-control.texi
src/language/lexer/macro.c

index 056ffb31310753dddafabbebf29ad12fe165f4f0..3b52205ef337430db413ae26bdbe8d9cd29cb05a 100644 (file)
@@ -71,6 +71,11 @@ The following functions may be used within the body:
   !TAIL(arg)
   !UNQUOTE(arg)
   !UPCASE(arg)
+
+The body may also include the following constructs:
+
+!IF (condition) !THEN true-expansion !ENDIF
+!IF (condition) !THEN true-expansion !ELSE false-expansion !ENDIF
 @end display
 
 The DEFINE command defines a macro that can later be called any number
@@ -87,15 +92,6 @@ 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.
 
-@menu
-* Macro Basics::
-* Macro Arguments::
-* Controlling Macro Expansion::
-* Macro Functions::
-* Macro Settings::
-* Macro Notes::
-@end menu
-
 @node Macro Basics
 @subsection Macro Basics
 
@@ -530,6 +526,50 @@ to uppercase.
 @end example
 @end deffn
 
+@node Macro Conditional Expansion
+@subsection Macro Conditional Expansion
+
+The @code{!IF} construct may be used inside a macro body to allow for
+conditional expansion.  It takes the following forms:
+
+@example
+!IF (condition) !THEN true-expansion !ENDIF
+!IF (condition) !THEN true-expansion !ELSE false-expansion !ENDIF
+@end example
+
+When the @var{condition} evaluates to true, @var{true-expansion} is
+expanded.  When it evaluates to false, @var{false-expansion} is
+expanded, if it is present.  The unquoted value @samp{0} is considered
+false, and all other values are considered true.
+
+Within @var{condition}, macros, macro arguments, and macro functions
+are expanded.  After expansion, the condition is evaluated as a macro
+expression that may use only the following operators, in descending
+order of operator precedence:
+
+@example
+()
+!EQ !NE !GT !LT !GE !LE = ~= <> > < >= <=
+!NOT ~
+!AND &
+!OR |
+@end example
+
+All of these operators yield a Boolean result, either @samp{0} for
+false or @samp{1} for true.
+
+If an operand is a quoted string, then the operator considers the
+contents of the quoted string; otherwise, it must be a single token.
+Thus, @code{1 = '1'} is true, and @code{'a b' = a b} is in error
+because the right-hand operand is two tokens.
+
+Comparisons in macro expressions are always string comparisons.  This
+can be surprising when the operands are numbers: e.g.@: @code{1 < 1.0}
+and @code{10 < 2} both evaluate to @samp{1} (true).
+
+Macro expression comparisons are case sensitive, so that @code{a = A}
+evaluates to @samp{0} (false).
+
 @node Macro Settings
 @subsection Macro Settings
 
@@ -551,7 +591,9 @@ of each macro in the input.  This feature can be useful for debugging
 macro definitions.
 
 MNEST (@pxref{SET MNEST}) limits the depth of expansion of macro
-calls, that is, the nesting level of macro expansion.
+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
 
index b120b35d725e432fecbb96f5f503ee9031cc0b2f..fdf76bacea12b2a07d5d3e6ead9811e7cdb84922 100644 (file)
@@ -26,6 +26,7 @@
 #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"
@@ -704,7 +705,7 @@ 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;
@@ -736,7 +737,7 @@ 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)
     {
@@ -797,7 +798,7 @@ 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
@@ -1052,6 +1053,314 @@ 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;
+    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,
+    .expand = ctx->expand,
+  };
+  struct string function_output = DS_EMPTY_INITIALIZER;
+  size_t function_consumed;
+  if (expand_macro_function (&fctx, &function_output, &function_consumed))
+    {
+      *tokens = p + function_consumed;
+      return ds_steal_cstr (&function_output);
+    }
+
+  *tokens = p + 1;
+  return ss_xstrdup (p->representation);
+}
+
+static char *
+macro_evaluate_logical (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 ? p->token.type : T_STOP;
+  if (op != T_EQUALS && op != T_EQ && op != T_NE && op != T_LT
+      && op != T_GT && op != T_LE && op != T_GE)
+    {
+      *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 && p->token.type == T_NOT)
+    {
+      p++;
+      negations++;
+    }
+
+  char *operand = macro_evaluate_logical (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 && p->token.type == T_AND)
+    {
+      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 && p->token.type == T_OR)
+    {
+      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, bool *expand)
+{
+  const struct expr_context ctx = {
+    .nesting_countdown = nesting_countdown,
+    .macros = macros,
+    .me = me,
+    .expand = expand,
+  };
+  return macro_evaluate_or (&ctx, tokens, *tokens + n_tokens);
+}
+
+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, 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 ("!IF")))
+    return 0;
+
+  p++;
+  char *result = macro_evaluate_expression (&p, end - p,
+                                            nesting_countdown, macros, me, 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, expand, exp);
+    }
+  return (end_if + 1) - tokens;
+}
+
 static void
 macro_expand (const struct macro_tokens *mts,
               int nesting_countdown, const struct macro_set *macros,
@@ -1104,6 +1413,15 @@ macro_expand (const struct macro_tokens *mts,
               i++;
               continue;
             }
+
+          size_t n = macro_expand_if (&mts->mts[i], mts->n - i,
+                                      nesting_countdown, macros, me, expand,
+                                      exp);
+          if (n > 0)
+            {
+              i += n - 1;
+              continue;
+            }
         }
 
       if (*expand)
@@ -1134,8 +1452,6 @@ 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. */
       struct parse_macro_function_ctx ctx = {
         .input = &mts->mts[i],
         .n_input = mts->n - i,