more parsing
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 27 Dec 2021 19:35:56 +0000 (11:35 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 30 Dec 2021 22:10:12 +0000 (14:10 -0800)
src/language/lexer/format-parser.c
src/language/lexer/lexer.c
src/language/lexer/lexer.h
src/language/stats/ctables.c

index 906d0f327168c5508f16859f46c52c3684987334..85579a2ab2116567d0eb0547c707e198361017ae 100644 (file)
@@ -99,10 +99,9 @@ parse_abstract_format_specifier (struct lexer *lexer,
   return ok;
 }
 
-/* Parses a format specifier from the token stream and returns
-   true only if successful.  Emits an error message on
-   failure.  The caller should call check_input_specifier() or
-   check_output_specifier() on the parsed format as
+/* Parses a format specifier from the token stream and returns true only if
+   successful.  Emits an error message on failure.  The caller should call
+   fmt_check_input() or fmt_check_output() on the parsed format as
    necessary.  */
 bool
 parse_format_specifier (struct lexer *lexer, struct fmt_spec *format)
index 329003406bf0281b0d01f99db81372bd4b5649b0..908556a7eeaab39472de12da5a0434b2ad03a6c5 100644 (file)
@@ -966,6 +966,236 @@ lex_force_num (struct lexer *lexer)
   return false;
 }
 
+/* If the current token is an number in the closed range [MIN,MAX], does
+   nothing and returns true.  Otherwise, reports an error and returns false.
+   If NAME is nonnull, then it is used in the error message. */
+bool
+lex_force_num_range_closed (struct lexer *lexer, const char *name,
+                            double min, double max)
+{
+  bool is_number = lex_is_number (lexer);
+  bool too_small = is_number && lex_number (lexer) < min;
+  bool too_big = is_number && lex_number (lexer) > max;
+  if (is_number && !too_small && !too_big)
+    return true;
+
+  if (min > max)
+    {
+      /* Weird, maybe a bug in the caller.  Just report that we needed an
+         number. */
+      if (name)
+        lex_error (lexer, _("Number expected for %s."), name);
+      else
+        lex_error (lexer, _("Number expected."));
+    }
+  else if (min == max)
+    {
+      if (name)
+        lex_error (lexer, _("Expected %g for %s."), min, name);
+      else
+        lex_error (lexer, _("Expected %g."), min);
+    }
+  else
+    {
+      bool report_lower_bound = min > -DBL_MAX || too_small;
+      bool report_upper_bound = max < DBL_MAX || too_big;
+
+      if (report_lower_bound && report_upper_bound)
+        {
+          if (name)
+            lex_error (lexer,
+                       _("Expected number between %g and %g for %s."),
+                       min, max, name);
+          else
+            lex_error (lexer, _("Expected number between %g and %g."),
+                       min, max);
+        }
+      else if (report_lower_bound)
+        {
+          if (min == 0)
+            {
+              if (name)
+                lex_error (lexer, _("Expected non-negative number for %s."),
+                           name);
+              else
+                lex_error (lexer, _("Expected non-negative number."));
+            }
+          else
+            {
+              if (name)
+                lex_error (lexer, _("Expected number %g or greater for %s."),
+                           min, name);
+              else
+                lex_error (lexer, _("Expected number %g or greater."), min);
+            }
+        }
+      else if (report_upper_bound)
+        {
+          if (name)
+            lex_error (lexer,
+                       _("Expected number less than or equal to %g for %s."),
+                       max, name);
+          else
+            lex_error (lexer, _("Expected number less than or equal to %g."),
+                       max);
+        }
+      else
+        {
+          if (name)
+            lex_error (lexer, _("Number expected for %s."), name);
+          else
+            lex_error (lexer, _("Number expected."));
+        }
+    }
+  return false;
+}
+
+/* If the current token is an number in the half-open range [MIN,MAX), does
+   nothing and returns true.  Otherwise, reports an error and returns false.
+   If NAME is nonnull, then it is used in the error message. */
+bool
+lex_force_num_range_halfopen (struct lexer *lexer, const char *name,
+                              double min, double max)
+{
+  bool is_number = lex_is_number (lexer);
+  bool too_small = is_number && lex_number (lexer) < min;
+  bool too_big = is_number && lex_number (lexer) >= max;
+  if (is_number && !too_small && !too_big)
+    return true;
+
+  if (min >= max)
+    {
+      /* Weird, maybe a bug in the caller.  Just report that we needed an
+         number. */
+      if (name)
+        lex_error (lexer, _("Number expected for %s."), name);
+      else
+        lex_error (lexer, _("Number expected."));
+    }
+  else
+    {
+      bool report_lower_bound = min > -DBL_MAX || too_small;
+      bool report_upper_bound = max < DBL_MAX || too_big;
+
+      if (report_lower_bound && report_upper_bound)
+        {
+          if (name)
+            lex_error (lexer, _("Expected number in [%g,%g) for %s."),
+                       min, max, name);
+          else
+            lex_error (lexer, _("Expected number in [%g,%g)."),
+                       min, max);
+        }
+      else if (report_lower_bound)
+        {
+          if (min == 0)
+            {
+              if (name)
+                lex_error (lexer, _("Expected non-negative number for %s."),
+                           name);
+              else
+                lex_error (lexer, _("Expected non-negative number."));
+            }
+          else
+            {
+              if (name)
+                lex_error (lexer, _("Expected number %g or greater for %s."),
+                           min, name);
+              else
+                lex_error (lexer, _("Expected number %g or greater."), min);
+            }
+        }
+      else if (report_upper_bound)
+        {
+          if (name)
+            lex_error (lexer,
+                       _("Expected number less than %g for %s."), max, name);
+          else
+            lex_error (lexer, _("Expected number less than %g."), max);
+        }
+      else
+        {
+          if (name)
+            lex_error (lexer, _("Number expected for %s."), name);
+          else
+            lex_error (lexer, _("Number expected."));
+        }
+    }
+  return false;
+}
+
+/* If the current token is an number in the open range (MIN,MAX], does
+   nothing and returns true.  Otherwise, reports an error and returns false.
+   If NAME is nonnull, then it is used in the error message. */
+bool
+lex_force_num_range_open (struct lexer *lexer, const char *name,
+                          double min, double max)
+{
+  bool is_number = lex_is_number (lexer);
+  bool too_small = is_number && lex_number (lexer) <= min;
+  bool too_big = is_number && lex_number (lexer) >= max;
+  if (is_number && !too_small && !too_big)
+    return true;
+
+  if (min >= max)
+    {
+      /* Weird, maybe a bug in the caller.  Just report that we needed an
+         number. */
+      if (name)
+        lex_error (lexer, _("Number expected for %s."), name);
+      else
+        lex_error (lexer, _("Number expected."));
+    }
+  else
+    {
+      bool report_lower_bound = min > -DBL_MAX || too_small;
+      bool report_upper_bound = max < DBL_MAX || too_big;
+
+      if (report_lower_bound && report_upper_bound)
+        {
+          if (name)
+            lex_error (lexer, _("Expected number in (%g,%g) for %s."),
+                       min, max, name);
+          else
+            lex_error (lexer, _("Expected number in (%g,%g)."), min, max);
+        }
+      else if (report_lower_bound)
+        {
+          if (min == 0)
+            {
+              if (name)
+                lex_error (lexer, _("Expected positive number for %s."), name);
+              else
+                lex_error (lexer, _("Expected positive number."));
+            }
+          else
+            {
+              if (name)
+                lex_error (lexer, _("Expected number greater than %g for %s."),
+                           min, name);
+              else
+                lex_error (lexer, _("Expected number greater than %g."), min);
+            }
+        }
+      else if (report_upper_bound)
+        {
+          if (name)
+            lex_error (lexer, _("Expected number less than %g for %s."),
+                       max, name);
+          else
+            lex_error (lexer, _("Expected number less than %g."), max);
+        }
+      else
+        {
+          if (name)
+            lex_error (lexer, _("Number expected for %s."), name);
+          else
+            lex_error (lexer, _("Number expected."));
+        }
+    }
+  return false;
+}
+
 /* If the current token is an identifier, does nothing and returns true.
    Otherwise, reports an error and returns false. */
 bool
index 5493db9b6a29175c7b522911c4782f70cf191a65..764da74b19db207100e9935606c817f97eb67047 100644 (file)
@@ -134,6 +134,12 @@ bool lex_force_int (struct lexer *) WARN_UNUSED_RESULT;
 bool lex_force_int_range (struct lexer *, const char *name,
                           long min, long max) WARN_UNUSED_RESULT;
 bool lex_force_num (struct lexer *) WARN_UNUSED_RESULT;
+bool lex_force_num_range_closed (struct lexer *, const char *name,
+                                 double min, double max) WARN_UNUSED_RESULT;
+bool lex_force_num_range_halfopen (struct lexer *, const char *name,
+                                   double min, double max) WARN_UNUSED_RESULT;
+bool lex_force_num_range_open (struct lexer *, const char *name,
+                               double min, double max) WARN_UNUSED_RESULT;
 bool lex_force_id (struct lexer *) WARN_UNUSED_RESULT;
 bool lex_force_string (struct lexer *) WARN_UNUSED_RESULT;
 bool lex_force_string_or_id (struct lexer *) WARN_UNUSED_RESULT;
index c5ac106e066a845db92aaaa5d33a2fc5dbb7a7ad..deb98beb9c0115cbd792fb6cfabbe3655c0bf094 100644 (file)
 
 #include <config.h>
 
+#include "data/dataset.h"
+#include "data/dictionary.h"
 #include "language/command.h"
+#include "language/lexer/format-parser.h"
 #include "language/lexer/lexer.h"
+#include "language/lexer/variable-parser.h"
 #include "libpspp/hmap.h"
 #include "libpspp/message.h"
 #include "output/pivot-table.h"
 
+#include "gl/minmax.h"
 #include "gl/xalloc.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 
+enum ctables_vlabel
+  {
+    CTVL_DEFAULT = SETTINGS_VALUE_SHOW_DEFAULT,
+    CTVL_NAME = SETTINGS_VALUE_SHOW_VALUE,
+    CTVL_LABEL = SETTINGS_VALUE_SHOW_LABEL,
+    CTVL_BOTH = SETTINGS_VALUE_SHOW_BOTH,
+    CTVL_NONE,
+  };
+static void UNUSED
+ctables_vlabel_unique (enum ctables_vlabel vlabel)
+{
+  /* This ensures that all of the values are unique. */
+  switch (vlabel)
+    {
+    case CTVL_DEFAULT:
+    case CTVL_NAME:
+    case CTVL_LABEL:
+    case CTVL_BOTH:
+    case CTVL_NONE:
+      abort ();
+    }
+}
+
 struct ctables
   {
     struct pivot_table_look *look;
@@ -39,27 +67,17 @@ struct ctables
        format.  Otherwise, this string is displayed. */
     char *missing;
 
-    /* Contains "struct ctables_vlabel" structs.  */
-    struct hmap vlabels;
+    enum ctables_vlabel *vlabels;
 
     bool mrsets_count_duplicates; /* MRSETS. */
     bool smissing_listwise;       /* SMISSING. */
     struct variable *base_weight; /* WEIGHT. */
-    double hide_threshold;        /* HIDESMALLCOUNTS. */
+    int hide_threshold;           /* HIDESMALLCOUNTS. */
 
     struct ctables_table *tables;
     size_t n_tables;
   };
 
-struct ctables_vlabel
-  {
-    struct hmap_node hmap_node; /* In struct ctables's 'vlabels' hmap. */
-    const char *name;           /* Variable name. */
-
-    /* SETTINGS_VALUE_SHOW_DEFAULT is interpreted as "none". */
-    enum settings_value_show show;
-  };
-
 struct ctables_postcompute
   {
     struct hmap_node hmap_node; /* In struct ctables's 'pcompute' hmap. */
@@ -112,11 +130,55 @@ struct ctables_postcompute_expr
       };
   };
 
+enum ctables_label_position
+  {
+    CTLP_NORMAL,
+    CTLP_OPPOSITE,
+    CTLP_LAYER,
+  };
+
 struct ctables_table
   {
     struct ctables_axis *axes[PIVOT_N_AXES];
 
+    enum pivot_axis_type slabels_position;
+    bool slabels_visible;
+
+    enum ctables_label_position row_labels;
+    enum ctables_label_position col_labels;
+
+    /* XXX CATEGORIES */
+
+    double cilevel;
+
+    char *caption;
+    char *corner;
+    char *title;
 
+    struct ctables_chisq *chisq;
+    struct ctables_pairwise *pairwise;
+  };
+
+/* Chi-square test (SIGTEST). */
+struct ctables_chisq
+  {
+    double alpha;
+    bool include_mrsets;
+    bool all_visible;
+  };
+
+/* Pairwise comparison test (COMPARETEST). */
+struct ctables_pairwise
+  {
+    enum { PROP, MEAN } type;
+    double alpha[2];
+    bool include_mrsets;
+    bool meansvariance_allcats;
+    bool all_visible;
+    enum { BONFERRONI = 1, BH } adjust;
+    bool merge;
+    bool apa_style;
+    bool show_sig;
   };
 
 struct ctables_axis
@@ -128,7 +190,7 @@ struct ctables_axis
         CTAO_MRSET,
 
         /* Nonterminals. */
-        CTAO_CONCAT,            /* + */
+        CTAO_STACK,             /* + */
         CTAO_NEST,              /* > */
       }
     op;
@@ -141,7 +203,7 @@ struct ctables_axis
             union
               {
                 struct variable *var;
-                struct mrset *mrset;
+                const struct mrset *mrset;
               };
 
             bool scale;
@@ -154,93 +216,956 @@ struct ctables_axis
       };
   };
 
+static void ctables_axis_destroy (struct ctables_axis *);
+
+#define SUMMARIES                                                       \
+    /* All variables. */                                                \
+    S(CTSF_COUNT, "COUNT")                                              \
+    S(CTSF_ECOUNT, "ECOUNT")                                            \
+    S(CTSF_ROWPCT_COUNT, "ROWPCT.COUNT")                                \
+    S(CTSF_COLPCT_COUNT, "COLPCT.COUNT")                                \
+    S(CTSF_TABLEPCT_COUNT, "TABLEPCT.COUNT")                            \
+    S(CTSF_SUBTABLEPCT_COUNT, "SUBTABLEPCT.COUNT")                      \
+    S(CTSF_LAYERPCT_COUNT, "LAYERPCT.COUNT")                            \
+    S(CTSF_LAYERROWPCT_COUNT, "LAYERROWPCT.COUNT")                      \
+    S(CTSF_LAYERCOLPCT_COUNT, "LAYERCOLPCT.COUNT")                      \
+    S(CTSF_ROWPCT_VALIDN, "ROWPCT.VALIDN")                              \
+    S(CTSF_COLPCT_VALIDN, "COLPCT.VALIDN")                              \
+    S(CTSF_TABLEPCT_VALIDN, "TABLEPCT.VALIDN")                          \
+    S(CTSF_SUBTABLEPCT_VALIDN, "SUBTABLEPCT.VALIDN")                    \
+    S(CTSF_LAYERPCT_VALIDN, "LAYERPCT.VALIDN")                          \
+    S(CTSF_LAYERROWPCT_VALIDN, "LAYERROWPCT.VALIDN")                    \
+    S(CTSF_LAYERCOLPCT_VALIDN, "LAYERCOLPCT.VALIDN")                    \
+    S(CTSF_ROWPCT_TOTALN, "ROWPCT.TOTALN")                              \
+    S(CTSF_COLPCT_TOTALN, "COLPCT.TOTALN")                              \
+    S(CTSF_TABLEPCT_TOTALN, "TABLEPCT.TOTALN")                          \
+    S(CTSF_SUBTABLEPCT_TOTALN, "SUBTABLEPCT.TOTALN")                    \
+    S(CTSF_LAYERPCT_TOTALN, "LAYERPCT.TOTALN")                          \
+    S(CTSF_LAYERROWPCT_TOTALN, "LAYERROWPCT.TOTALN")                    \
+    S(CTSF_LAYERCOLPCT_TOTALN, "LAYERCOLPCT.TOTALN")                    \
+                                                                        \
+    /* Scale variables, totals, and subtotals. */                       \
+    S(CTSF_MAXIMUM, "!MAXIMUM")                                         \
+    S(CTSF_MEAN, "!MEAN")                                               \
+    S(CTSF_MEDIAN, "!MEDIAN")                                           \
+    S(CTSF_MINIMUM, "!MINIMUM")                                         \
+    S(CTSF_MISSING, "!MISSING")                                         \
+    S(CTSF_MODE, "!MODE")                                               \
+    S(CTSF_PTILE, "!PTILE")                                             \
+    S(CTSF_RANGE, "!RANGE")                                             \
+    S(CTSF_SEMAN, "!SEMAN")                                             \
+    S(CTSF_STDDEV, "!STDDEV")                                           \
+    S(CTSF_SUM, "!SUM")                                                 \
+    S(CSTF_TOTALN, "!TOTALN")                                           \
+    S(CTSF_ETOTALN, "!ETOTALN")                                         \
+    S(CTSF_VALIDN, "!VALIDN")                                           \
+    S(CTSF_EVALIDN, "!EVALIDN")                                         \
+    S(CTSF_VARIANCE, "!VARIANCE")                                       \
+    S(CTSF_ROWPCT_SUM, "ROWPCT.SUM")                                    \
+    S(CTSF_COLPCT_SUM, "COLPCT.SUM")                                    \
+    S(CTSF_TABLEPCT_SUM, "TABLEPCT.SUM")                                \
+    S(CTSF_SUBTABLEPCT_SUM, "SUBTABLEPCT.SUM")                          \
+    S(CTSF_LAYERPCT_SUM, "LAYERPCT.SUM")                                \
+    S(CTSF_LAYERROWPCT_SUM, "LAYERROWPCT.SUM")                          \
+    S(CTSF_LAYERCOLPCT_SUM, "LAYERCOLPCT.SUM")                          \
+                                                                        \
+    /* Multiple response sets. */                                       \
+    S(CTSF_ROWPCT_RESPONSES, "ROWPCT.RESPONSES")                        \
+    S(CTSF_COLPCT_RESPONSES, "COLPCT.RESPONSES")                        \
+    S(CTSF_TABLEPCT_RESPONSES, "TABLEPCT.RESPONSES")                    \
+    S(CTSF_SUBTABLEPCT_RESPONSES, "SUBTABLEPCT.RESPONSES")              \
+    S(CTSF_LAYERPCT_RESPONSES, "LAYERPCT.RESPONSES")                    \
+    S(CTSF_LAYERROWPCT_RESPONSES, "LAYERROWPCT.RESPONSES")              \
+    S(CTSF_LAYERCOLPCT_RESPONSES, "LAYERCOLPCT.RESPONSES")              \
+    S(CTSF_ROWPCT_RESPONSES_COUNT, "ROWPCT.RESPONSES.COUNT")            \
+    S(CTSF_COLPCT_RESPONSES_COUNT, "COLPCT.RESPONSES.COUNT")            \
+    S(CTSF_TABLEPCT_RESPONSES_COUNT, "TABLEPCT.RESPONSES.COUNT")        \
+    S(CTSF_SUBTABLEPCT_RESPONSES_COUNT, "SUBTABLEPCT.RESPONSES.COUNT")  \
+    S(CTSF_LAYERPCT_RESPONSES_COUNT, "LAYERPCT.RESPONSES.COUNT")        \
+    S(CTSF_LAYERROWPCT_RESPONSES_COUNT, "LAYERROWPCT.RESPONSES.COUNT")  \
+    S(CTSF_LAYERCOLPCT_RESPONSES_COUNT, "LAYERCOLPCT.RESPONSES.COUNT")  \
+    S(CTSF_ROWPCT_COUNT_RESPONSES, "ROWPCT.COUNT.RESPONSES")            \
+    S(CTSF_COLPCT_COUNT_RESPONSES, "COLPCT.COUNT.RESPONSES")            \
+    S(CTSF_TABLEPCT_COUNT_RESPONSES, "TABLEPCT.COUNT.RESPONSES")        \
+    S(CTSF_SUBTABLEPCT_COUNT_RESPONSES, "SUBTABLEPCT.COUNT.RESPONSES")  \
+    S(CTSF_LAYERPCT_COUNT_RESPONSES, "LAYERPCT.COUNT.RESPONSES")        \
+    S(CTSF_LAYERROWPCT_COUNT_RESPONSES, "LAYERROWPCT.COUNT.RESPONSES")  \
+    S(CTSF_LAYERCOLPCT_COUNT_RESPONSES, "LAYERCOLPCT.COUNT.RESPONSES")
+
+enum ctables_summary_function
+  {
+#define S(ENUM, NAME) ENUM,
+    SUMMARIES
+#undef S
+  };
+
+enum {
+#define S(ENUM, NAME) +1
+  N_CTSF_FUNCTIONS = SUMMARIES
+#undef S
+};
+
 struct ctables_summary
   {
-    enum ctables_summary_function
+    enum ctables_summary_function function;
+    char *label;
+    struct fmt_spec format;     /* XXX extra CTABLES formats */
+  };
+
+static void
+ctables_summary_uninit (struct ctables_summary *s)
+{
+  if (s)
+    free (s->label);
+}
+
+static bool
+parse_col_width (struct lexer *lexer, const char *name, double *width)
+{
+  lex_match (lexer, T_EQUALS);
+  if (lex_match_id (lexer, "DEFAULT"))
+    *width = SYSMIS;
+  else if (lex_force_num_range_closed (lexer, name, 0, DBL_MAX))
+    {
+      *width = lex_number (lexer);
+      lex_get (lexer);
+    }
+  else
+    return false;
+
+  return true;
+}
+
+static bool
+parse_bool (struct lexer *lexer, bool *b)
+{
+  if (lex_match_id (lexer, "NO"))
+    *b = false;
+  else if (lex_match_id (lexer, "YES"))
+    *b = true;
+  else
+    {
+      lex_error_expecting (lexer, "YES", "NO");
+      return false;
+    }
+  return true;
+}
+
+static bool
+parse_ctables_summary_function (struct lexer *lexer,
+                                enum ctables_summary_function *f)
+{
+  struct pair
+    {
+      enum ctables_summary_function function;
+      struct substring name;
+    };
+  static struct pair names[] = {
+#define S(ENUM, NAME) { ENUM, SS_LITERAL_INITIALIZER (NAME) },
+    SUMMARIES
+
+    /* The .COUNT suffix may be omitted. */
+    S(CTSF_ROWPCT_COUNT, "ROWPCT")
+    S(CTSF_COLPCT_COUNT, "COLPCT")
+    S(CTSF_TABLEPCT_COUNT, "TABLEPCT")
+    S(CTSF_SUBTABLEPCT_COUNT, "SUBTABLEPCT")
+    S(CTSF_LAYERPCT_COUNT, "LAYERPCT")
+    S(CTSF_LAYERROWPCT_COUNT, "LAYERROWPCT")
+    S(CTSF_LAYERCOLPCT_COUNT, "LAYERCOLPCT")
+#undef S
+  };
+
+  if (!lex_force_id (lexer))
+    return false;
+
+  for (size_t i = 0; i < sizeof names / sizeof *names; i++)
+    if (ss_equals_case (names[i].name, lex_tokss (lexer)))
       {
-        /* All variables. */
-        CTSF_COUNT,
-        CTSF_ECOUNT,
-        CTSF_ROWPCT_COUNT,
-        CTSF_COLPCT_COUNT,
-        CTSF_TABLEPCT_COUNT,
-        CTSF_SUBTABLEPCT_COUNT,
-        CTSF_LAYERPCT_COUNT,
-        CTSF_LAYERROWPCT_COUNT,
-        CTSF_LAYERCOLPCT_COUNT,
-        CTSF_ROWPCT_VALIDN,
-        CTSF_COLPCT_VALIDN,
-        CTSF_TABLEPCT_VALIDN,
-        CTSF_SUBTABLEPCT_VALIDN,
-        CTSF_LAYERPCT_VALIDN,
-        CTSF_LAYERROWPCT_VALIDN,
-        CTSF_LAYERCOLPCT_VALIDN,
-        CTSF_ROWPCT_TOTALN,
-        CTSF_COLPCT_TOTALN,
-        CTSF_TABLEPCT_TOTALN,
-        CTSF_SUBTABLEPCT_TOTALN,
-        CTSF_LAYERPCT_TOTALN,
-        CTSF_LAYERROWPCT_TOTALN,
-        CTSF_LAYERCOLPCT_TOTALN,
-
-        /* Scale variables, totals, and subtotals. */
-        CTSF_MAXIMUM,
-        CTSF_MEAN,
-        CTSF_MEDIAN,
-        CTSF_MINIMUM,
-        CTSF_MISSING,
-        CTSF_MODE,
-        CTSF_PTILE,
-        CTSF_RANGE,
-        CTSF_SEMAN,
-        CTSF_STDDEV,
-        CTSF_SUM,
-        CSTF_TOTALN,
-        CTSF_ETOTALN,
-        CTSF_VALIDN,
-        CTSF_EVALIDN,
-        CTSF_VARIANCE,
-        CTSF_ROWPCT_SUM,
-        CTSF_COLPCT_SUM,
-        CTSF_TABLEPCT_SUM,
-        CTSF_SUBTABLEPCT_SUM,
-        CTSF_LAYERPCT_SUM,
-        CTSF_LAYERROWPCT_SUM,
-        CTSF_LAYERCOLPCT_SUM,
-
-        /* Multiple response sets. */
-        CTSF_ROWPCT_RESPONSES,
-        CTSF_COLPCT_RESPONSES,
-        CTSF_TABLEPCT_RESPONSES,
-        CTSF_SUBTABLEPCT_RESPONSES,
-        CTSF_LAYERPCT_RESPONSES,
-        CTSF_LAYERROWPCT_RESPONSES,
-        CTSF_LAYERCOLPCT_RESPONSES,
-        CTSF_ROWPCT_RESPONSES_COUNT,
-        CTSF_COLPCT_RESPONSES_COUNT,
-        CTSF_TABLEPCT_RESPONSES_COUNT,
-        CTSF_SUBTABLEPCT_RESPONSES_COUNT,
-        CTSF_LAYERPCT_RESPONSES_COUNT,
-        CTSF_LAYERROWPCT_RESPONSES_COUNT,
-        CTSF_LAYERCOLPCT_RESPONSES_COUNT,
-        CTSF_ROWPCT_COUNT_RESPONSES,
-        CTSF_COLPCT_COUNT_RESPONSES,
-        CTSF_TABLEPCT_COUNT_RESPONSES,
-        CTSF_SUBTABLEPCT_COUNT_RESPONSES,
-        CTSF_LAYERPCT_COUNT_RESPONSES,
-        CTSF_LAYERROWPCT_COUNT_RESPONSES,
-        CTSF_LAYERCOLPCT_COUNT_RESPONSES,
+        *f = names[i].function;
+        return true;
       }
-    function;
 
-    char *label;
-    struct fmt_spec format;     /* XXX extra CTABLES formats */
+  lex_error (lexer, _("Expecting summary function name."));
+  return false;
+}
+
+static void
+ctables_axis_destroy (struct ctables_axis *axis)
+{
+  if (!axis)
+    return;
+
+  switch (axis->op)
+    {
+    case CTAO_VAR:
+    case CTAO_MRSET:
+      for (size_t i = 0; i < axis->n_summaries; i++)
+        ctables_summary_uninit (&axis->summaries[i]);
+      free (axis->summaries);
+      break;
+
+    case CTAO_STACK:
+    case CTAO_NEST:
+      ctables_axis_destroy (axis->subs[0]);
+      ctables_axis_destroy (axis->subs[1]);
+      break;
+    }
+  free (axis);
+}
+
+static struct ctables_axis *
+ctables_axis_new_nonterminal (enum ctables_axis_op op,
+                              struct ctables_axis *sub0,
+                              struct ctables_axis *sub1)
+{
+  struct ctables_axis *axis = xmalloc (sizeof *axis);
+  *axis = (struct ctables_axis) { .op = op, .subs = { sub0, sub1 } };
+  return axis;
+}
+
+struct ctables_axis_parse_ctx
+  {
+    struct lexer *lexer;
+    struct dictionary *dict;
+    struct ctables *ct;
+    struct ctables_table *t;
   };
 
+static struct ctables_summary *
+add_summary (struct ctables_axis *axis, size_t *allocated_summaries)
+{
+  if (axis->n_summaries >= *allocated_summaries)
+    axis->summaries = x2nrealloc (axis->summaries, allocated_summaries,
+                                  sizeof *axis->summaries);
+
+  struct ctables_summary *s = &axis->summaries[axis->n_summaries++];
+  *s = (struct ctables_summary) { .function = CTSF_COUNT };
+  return s;
+}
+
+static struct ctables_axis *ctables_axis_parse_stack (
+  struct ctables_axis_parse_ctx *);
+
+static struct ctables_axis *
+ctables_axis_parse_primary (struct ctables_axis_parse_ctx *ctx)
+{
+  if (lex_match (ctx->lexer, T_LPAREN))
+    {
+      struct ctables_axis *sub = ctables_axis_parse_stack (ctx);
+      if (!sub || !lex_force_match (ctx->lexer, T_RPAREN))
+        {
+          ctables_axis_destroy (sub);
+          return NULL;
+        }
+      return sub;
+    }
+
+  if (!lex_force_id (ctx->lexer))
+    return NULL;
+
+  const struct mrset *mrset = NULL;
+  struct variable *var = NULL;
+  if (ss_starts_with (lex_tokss (ctx->lexer), ss_cstr ("$")))
+    {
+      mrset = dict_lookup_mrset (ctx->dict, lex_tokcstr (ctx->lexer));
+      if (!mrset)
+        {
+          lex_error (ctx->lexer, _("'%s' is not the name of a "
+                                   "multiple-response set in the active file "
+                                   "dictionary."),
+                     lex_tokcstr (ctx->lexer));
+          return NULL;
+        }
+      lex_get (ctx->lexer);
+    }
+  else
+    {
+      var = parse_variable (ctx->lexer, ctx->dict);
+      if (!var)
+        return NULL;
+    }
+
+  struct ctables_axis *axis = xmalloc (sizeof *axis);
+  if (mrset)
+    *axis = (struct ctables_axis) { .op = CTAO_MRSET, .mrset = mrset };
+  else
+    *axis = (struct ctables_axis) { .op = CTAO_VAR, .var = var };
+
+  /* XXX should figure out default measures by reading data */
+  axis->scale = (mrset ? false
+                 : lex_match_phrase (ctx->lexer, "[S]") ? true
+                 : lex_match_phrase (ctx->lexer, "[C]") ? false
+                 : var_get_measure (var) == MEASURE_SCALE);
+
+  size_t allocated_summaries = 0;
+  if (lex_match (ctx->lexer, T_LBRACK))
+    {
+      do
+        {
+          struct ctables_summary *s = add_summary (axis, &allocated_summaries);
+          if (!parse_ctables_summary_function (ctx->lexer, &s->function))
+            goto error;
+          if (lex_is_string (ctx->lexer))
+            {
+              s->label = ss_xstrdup (lex_tokss (ctx->lexer));
+              lex_get (ctx->lexer);
+            }
+          if (lex_token (ctx->lexer) == T_ID)
+            {
+              if (!parse_format_specifier (ctx->lexer, &s->format)
+                  || !fmt_check_output (&s->format)
+                  || !fmt_check_type_compat (&s->format, VAL_NUMERIC))
+                goto error;
+            }
+          lex_match (ctx->lexer, T_COMMA);
+        }
+      while (!lex_match (ctx->lexer, T_RBRACK));
+    }
+  else
+    {
+      struct ctables_summary *s = add_summary (axis, &allocated_summaries);
+      s->function = axis->scale ? CTSF_MEAN : CTSF_COUNT;
+      s->label = xstrdup (axis->scale ? _("Mean") : _("Count"));
+      s->format = (struct fmt_spec) { .type = FMT_F, .w = 40 };
+    }
+  return axis;
+
+error:
+  ctables_axis_destroy (axis);
+  return NULL;
+}
+
+static struct ctables_axis *
+ctables_axis_parse_nest (struct ctables_axis_parse_ctx *ctx)
+{
+  struct ctables_axis *lhs = ctables_axis_parse_primary (ctx);
+  if (!lhs)
+    return NULL;
+
+  while (lex_match (ctx->lexer, T_PLUS))
+    {
+      struct ctables_axis *rhs = ctables_axis_parse_primary (ctx);
+      if (!rhs)
+        return NULL;
+
+      lhs = ctables_axis_new_nonterminal (CTAO_NEST, lhs, rhs);
+    }
+
+  return lhs;
+}
+
+static struct ctables_axis *
+ctables_axis_parse_stack (struct ctables_axis_parse_ctx *ctx)
+{
+  struct ctables_axis *lhs = ctables_axis_parse_nest (ctx);
+  if (!lhs)
+    return NULL;
+
+  while (lex_match (ctx->lexer, T_PLUS))
+    {
+      struct ctables_axis *rhs = ctables_axis_parse_nest (ctx);
+      if (!rhs)
+        return NULL;
+
+      lhs = ctables_axis_new_nonterminal (CTAO_STACK, lhs, rhs);
+    }
+
+  return lhs;
+}
+
+static bool
+ctables_axis_parse (struct lexer *lexer, struct dictionary *dict,
+                    struct ctables *ct, struct ctables_table *t,
+                    enum pivot_axis_type a)
+{
+  if (lex_token (lexer) == T_BY
+      || lex_token (lexer) == T_SLASH
+      || lex_token (lexer) == T_ENDCMD)
+    return true;
+
+  struct ctables_axis_parse_ctx ctx = {
+    .lexer = lexer,
+    .dict = dict,
+    .ct = ct,
+    .t = t
+  };
+  t->axes[a] = ctables_axis_parse_stack (&ctx);
+  return t->axes[a] != NULL;
+}
+
 int
 cmd_ctables (struct lexer *lexer, struct dataset *ds)
 {
-  
+  size_t n_vars = dict_get_n_vars (dataset_dict (ds));
+  enum ctables_vlabel *vlabels = xnmalloc (n_vars, sizeof *vlabels);
+  for (size_t i = 0; n_vars; i++)
+    vlabels[i] = CTVL_DEFAULT;
+
+  struct ctables *ct = xmalloc (sizeof *ct);
+  *ct = (struct ctables) {
+    .look = pivot_table_look_unshare (pivot_table_look_ref (
+                                        pivot_table_look_get_default ())),
+    .vlabels = vlabels,
+    .hide_threshold = 5,
+  };
+
+  if (!lex_force_match (lexer, T_SLASH))
+    goto error;
+
+  while (!lex_match_id (lexer, "TABLE"))
+    {
+      if (lex_match_id (lexer, "FORMAT"))
+        {
+          double widths[2] = { SYSMIS, SYSMIS };
+          double units_per_inch = 72.0;
+
+          while (lex_token (lexer) != T_SLASH)
+            {
+              if (lex_match_id (lexer, "MINCOLWIDTH"))
+                {
+                  if (!parse_col_width (lexer, "MINCOLWIDTH", &widths[0]))
+                    goto error;
+                }
+              else if (lex_match_id (lexer, "MAXCOLWIDTH"))
+                {
+                  if (!parse_col_width (lexer, "MAXCOLWIDTH", &widths[1]))
+                    goto error;
+                }
+              else if (lex_match_id (lexer, "UNITS"))
+                {
+                  lex_match (lexer, T_EQUALS);
+                  if (lex_match_id (lexer, "POINTS"))
+                    units_per_inch = 72.0;
+                  else if (lex_match_id (lexer, "INCHES"))
+                    units_per_inch = 1.0;
+                  else if (lex_match_id (lexer, "CM"))
+                    units_per_inch = 2.54;
+                  else
+                    {
+                      lex_error_expecting (lexer, "POINTS", "INCHES", "CM");
+                      goto error;
+                    }
+                }
+              else if (lex_match_id (lexer, "EMPTY"))
+                {
+                  free (ct->zero);
+                  ct->zero = NULL;
+
+                  lex_match (lexer, T_EQUALS);
+                  if (lex_match_id (lexer, "ZERO"))
+                    {
+                      /* Nothing to do. */
+                    }
+                  else if (lex_match_id (lexer, "BLANK"))
+                    ct->zero = xstrdup ("");
+                  else if (lex_force_string (lexer))
+                    {
+                      ct->zero = ss_xstrdup (lex_tokss (lexer));
+                      lex_get (lexer);
+                    }
+                  else
+                    goto error;
+                }
+              else if (lex_match_id (lexer, "MISSING"))
+                {
+                  lex_match (lexer, T_EQUALS);
+                  if (!lex_force_string (lexer))
+                    goto error;
+
+                  free (ct->missing);
+                  ct->missing = (strcmp (lex_tokcstr (lexer), ".")
+                                 ? ss_xstrdup (lex_tokss (lexer))
+                                 : NULL);
+                  lex_get (lexer);
+                }
+              else
+                {
+                  lex_error_expecting (lexer, "MINCOLWIDTH", "MAXCOLWIDTH",
+                                       "UNITS", "EMPTY", "MISSING");
+                  goto error;
+                }
+            }
+
+          if (widths[0] != SYSMIS && widths[1] != SYSMIS
+              && widths[0] > widths[1])
+            {
+              msg (SE, _("MINCOLWIDTH must not be greater than MAXCOLWIDTH."));
+              goto error;
+            }
+
+          for (size_t i = 0; i < 2; i++)
+            if (widths[i] != SYSMIS)
+              {
+                int *wr = ct->look->width_ranges[TABLE_HORZ];
+                wr[i] = widths[i] / units_per_inch * 96.0;
+                if (wr[0] > wr[1])
+                  wr[!i] = wr[i];
+              }
+        }
+      else if (lex_match_id (lexer, "VLABELS"))
+        {
+          if (!lex_force_match_id (lexer, "VARIABLES"))
+            goto error;
+          lex_match (lexer, T_EQUALS);
+
+          struct variable **vars;
+          size_t n_vars;
+          if (!parse_variables (lexer, dataset_dict (ds), &vars, &n_vars,
+                                PV_NO_SCRATCH))
+            goto error;
+
+          if (!lex_force_match_id (lexer, "DISPLAY"))
+            {
+              free (vars);
+              goto error;
+            }
+          lex_match (lexer, T_EQUALS);
+
+          enum ctables_vlabel vlabel;
+          if (lex_match_id (lexer, "DEFAULT"))
+            vlabel = CTVL_DEFAULT;
+          else if (lex_match_id (lexer, "NAME"))
+            vlabel = CTVL_NAME;
+          else if (lex_match_id (lexer, "LABEL"))
+            vlabel = CTVL_LABEL;
+          else if (lex_match_id (lexer, "BOTH"))
+            vlabel = CTVL_BOTH;
+          else if (lex_match_id (lexer, "NONE"))
+            vlabel = CTVL_NONE;
+          else
+            {
+              lex_error_expecting (lexer, "DEFAULT", "NAME", "LABEL",
+                                   "BOTH", "NONE");
+              free (vars);
+              goto error;
+            }
+
+          for (size_t i = 0; i < n_vars; i++)
+            ct->vlabels[var_get_dict_index (vars[i])] = vlabel;
+          free (vars);
+        }
+      else if (lex_match_id (lexer, "MRSETS"))
+        {
+          if (!lex_force_match_id (lexer, "COUNTDUPLICATES"))
+            goto error;
+          lex_match (lexer, T_EQUALS);
+          if (!parse_bool (lexer, &ct->mrsets_count_duplicates))
+            goto error;
+        }
+      else if (lex_match_id (lexer, "SMISSING"))
+        {
+          if (lex_match_id (lexer, "VARIABLE"))
+            ct->smissing_listwise = false;
+          else if (lex_match_id (lexer, "LISTWISE"))
+            ct->smissing_listwise = true;
+          else
+            {
+              lex_error_expecting (lexer, "VARIABLE", "LISTWISE");
+              goto error;
+            }
+        }
+      /* XXX PCOMPUTE */
+      else if (lex_match_id (lexer, "WEIGHT"))
+        {
+          if (!lex_force_match_id (lexer, "VARIABLE"))
+            goto error;
+          lex_match (lexer, T_EQUALS);
+          ct->base_weight = parse_variable (lexer, dataset_dict (ds));
+          if (!ct->base_weight)
+            goto error;
+        }
+      else if (lex_match_id (lexer, "HIDESMALLCOUNTS"))
+        {
+          if (!lex_force_match_id (lexer, "COUNT"))
+            goto error;
+          lex_match (lexer, T_EQUALS);
+          if (!lex_force_int_range (lexer, "HIDESMALLCOUNTS COUNT", 2, INT_MAX))
+            goto error;
+          ct->hide_threshold = lex_integer (lexer);
+          lex_get (lexer);
+        }
+      else
+        {
+          lex_error_expecting (lexer, "FORMAT", "VLABELS", "MRSETS",
+                               "SMISSING", "PCOMPUTE", "PPROPERTIES",
+                               "WEIGHT", "HIDESMALLCOUNTS", "TABLE");
+          goto error;
+        }
+
+      if (!lex_force_match (lexer, T_SLASH))
+        goto error;
+    }
+
+  size_t allocated_tables = 0;
+  do
+    {
+      if (ct->n_tables >= allocated_tables)
+        ct->tables = x2nrealloc (ct->tables, &allocated_tables,
+                                 sizeof *ct->tables);
+
+      struct ctables_table *t = &ct->tables[ct->n_tables++];
+      *t = (struct ctables_table) {
+        .slabels_position = PIVOT_AXIS_COLUMN,
+        .slabels_visible = true,
+        .row_labels = CTLP_NORMAL,
+        .col_labels = CTLP_NORMAL,
+        .cilevel = 95,
+      };
+
+      lex_match (lexer, T_EQUALS);
+      if (!ctables_axis_parse (lexer, dataset_dict (ds), ct, t, PIVOT_AXIS_ROW))
+        goto error;
+
+      if (lex_match (lexer, T_BY))
+        {
+          if (!ctables_axis_parse (lexer, dataset_dict (ds),
+                                   ct, t, PIVOT_AXIS_COLUMN))
+            goto error;
+
+          if (lex_match (lexer, T_BY))
+            {
+              if (!ctables_axis_parse (lexer, dataset_dict (ds),
+                                       ct, t, PIVOT_AXIS_LAYER))
+                goto error;
+            }
+        }
+      if (!lex_force_match (lexer, T_SLASH))
+        goto error;
+
+      /* XXX Validate axes. */
+      while (!lex_match_id (lexer, "TABLE") && lex_token (lexer) != T_ENDCMD)
+        {
+          if (lex_match_id (lexer, "SLABELS"))
+            {
+              while (lex_token (lexer) != T_SLASH)
+                {
+                  if (lex_match_id (lexer, "POSITION"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "COLUMN"))
+                        t->slabels_position = PIVOT_AXIS_COLUMN;
+                      else if (lex_match_id (lexer, "ROW"))
+                        t->slabels_position = PIVOT_AXIS_ROW;
+                      else if (lex_match_id (lexer, "LAYER"))
+                        t->slabels_position = PIVOT_AXIS_LAYER;
+                      else
+                        {
+                          lex_error_expecting (lexer, "COLUMN", "ROW",
+                                               "LAYER");
+                          goto error;
+                        }
+                    }
+                  else if (lex_match_id (lexer, "VISIBLE"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (!parse_bool (lexer, &t->slabels_visible))
+                        goto error;
+                    }
+                  else
+                    {
+                      lex_error_expecting (lexer, "POSITION", "VISIBLE");
+                      goto error;
+                    }
+                }
+            }
+          else if (lex_match_id (lexer, "CLABELS"))
+            {
+              while (lex_token (lexer) != T_SLASH)
+                {
+                  if (lex_match_id (lexer, "AUTO"))
+                    t->row_labels = t->col_labels = CTLP_NORMAL;
+                  else if (lex_match_id (lexer, "ROWLABELS"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "OPPOSITE"))
+                        t->row_labels = CTLP_OPPOSITE;
+                      else if (lex_match_id (lexer, "LAYER"))
+                        t->row_labels = CTLP_LAYER;
+                      else
+                        {
+                          lex_error_expecting (lexer, "OPPOSITE", "LAYER");
+                          goto error;
+                        }
+                    }
+                  else if (lex_match_id (lexer, "COLLABELS"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "OPPOSITE"))
+                        t->col_labels = CTLP_OPPOSITE;
+                      else if (lex_match_id (lexer, "LAYER"))
+                        t->col_labels = CTLP_LAYER;
+                      else
+                        {
+                          lex_error_expecting (lexer, "OPPOSITE", "LAYER");
+                          goto error;
+                        }
+                    }
+                  else
+                    {
+                      lex_error_expecting (lexer, "AUTO", "ROWLABELS",
+                                           "COLLABELS");
+                      goto error;
+                    }
+                }
+            }
+          else if (lex_match_id (lexer, "CRITERIA"))
+            {
+              if (!lex_force_match_id (lexer, "CILEVEL"))
+                goto error;
+              lex_match (lexer, T_EQUALS);
+
+              if (!lex_force_num_range_halfopen (lexer, "CILEVEL", 0, 100))
+                goto error;
+              t->cilevel = lex_number (lexer);
+              lex_get (lexer);
+            }
+          else if (lex_match_id (lexer, "TITLES"))
+            {
+              do
+                {
+                  char **textp;
+                  if (lex_match_id (lexer, "CAPTION"))
+                    textp = &t->caption;
+                  else if (lex_match_id (lexer, "CORNER"))
+                    textp = &t->corner;
+                  else if (lex_match_id (lexer, "TITLE"))
+                    textp = &t->title;
+                  else
+                    {
+                      lex_error_expecting (lexer, "CAPTION", "CORNER", "TITLE");
+                      goto error;
+                    }
+                  lex_match (lexer, T_EQUALS);
+
+                  struct string s = DS_EMPTY_INITIALIZER;
+                  while (lex_is_string (lexer))
+                    {
+                      if (!ds_is_empty (&s))
+                        ds_put_byte (&s, ' ');
+                      ds_put_substring (&s, lex_tokss (lexer));
+                      lex_get (lexer);
+                    }
+                  free (*textp);
+                  *textp = ds_steal_cstr (&s);
+                }
+              while (lex_token (lexer) != T_SLASH
+                     && lex_token (lexer) != T_ENDCMD);
+            }
+          else if (lex_match_id (lexer, "SIGTEST"))
+            {
+              if (!t->chisq)
+                {
+                  t->chisq = xmalloc (sizeof *t->chisq);
+                  *t->chisq = (struct ctables_chisq) {
+                    .alpha = .05,
+                    .include_mrsets = true,
+                    .all_visible = true,
+                  };
+                }
+
+              do
+                {
+                  if (lex_match_id (lexer, "TYPE"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (!lex_force_match_id (lexer, "CHISQUARE"))
+                        goto error;
+                    }
+                  else if (lex_match_id (lexer, "ALPHA"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (!lex_force_num_range_halfopen (lexer, "ALPHA", 0, 1))
+                        goto error;
+                      t->chisq->alpha = lex_number (lexer);
+                      lex_get (lexer);
+                    }
+                  else if (lex_match_id (lexer, "INCLUDEMRSETS"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (parse_bool (lexer, &t->chisq->include_mrsets))
+                        goto error;
+                    }
+                  else if (lex_match_id (lexer, "CATEGORIES"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "ALLVISIBLE"))
+                        t->chisq->all_visible = true;
+                      else if (lex_match_id (lexer, "SUBTOTALS"))
+                        t->chisq->all_visible = false;
+                      else
+                        {
+                          lex_error_expecting (lexer,
+                                               "ALLVISIBLE", "SUBTOTALS");
+                          goto error;
+                        }
+                    }
+                  else
+                    {
+                      lex_error_expecting (lexer, "TYPE", "ALPHA",
+                                           "INCLUDEMRSETS", "CATEGORIES");
+                      goto error;
+                    }
+                }
+              while (lex_token (lexer) != T_SLASH
+                     && lex_token (lexer) != T_ENDCMD);
+            }
+          else if (lex_match_id (lexer, "COMPARETEST"))
+            {
+              if (!t->pairwise)
+                {
+                  t->pairwise = xmalloc (sizeof *t->pairwise);
+                  *t->pairwise = (struct ctables_pairwise) {
+                    .type = PROP,
+                    .alpha = { .05, .05 },
+                    .adjust = BONFERRONI,
+                    .include_mrsets = true,
+                    .meansvariance_allcats = true,
+                    .all_visible = true,
+                    .merge = false,
+                    .apa_style = true,
+                    .show_sig = false,
+                  };
+                }
+
+              do
+                {
+                  if (lex_match_id (lexer, "TYPE"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "PROP"))
+                        t->pairwise->type = PROP;
+                      else if (lex_match_id (lexer, "MEAN"))
+                        t->pairwise->type = MEAN;
+                      else
+                        {
+                          lex_error_expecting (lexer, "PROP", "MEAN");
+                          goto error;
+                        }
+                    }
+                  else if (lex_match_id (lexer, "ALPHA"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+
+                      if (!lex_force_num_range_open (lexer, "ALPHA", 0, 1))
+                        goto error;
+                      double a0 = lex_number (lexer);
+                      lex_get (lexer);
+
+                      lex_match (lexer, T_COMMA);
+                      if (lex_is_number (lexer))
+                        {
+                          if (!lex_force_num_range_open (lexer, "ALPHA", 0, 1))
+                            goto error;
+                          double a1 = lex_number (lexer);
+                          lex_get (lexer);
+
+                          t->pairwise->alpha[0] = MIN (a0, a1);
+                          t->pairwise->alpha[1] = MAX (a0, a1);
+                        }
+                      else
+                        t->pairwise->alpha[0] = t->pairwise->alpha[1] = a0;
+                    }
+                  else if (lex_match_id (lexer, "ADJUST"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "BONFERRONI"))
+                        t->pairwise->adjust = BONFERRONI;
+                      else if (lex_match_id (lexer, "BH"))
+                        t->pairwise->adjust = BH;
+                      else if (lex_match_id (lexer, "NONE"))
+                        t->pairwise->adjust = 0;
+                      else
+                        {
+                          lex_error_expecting (lexer, "BONFERRONI", "BH",
+                                               "NONE");
+                          goto error;
+                        }
+                    }
+                  else if (lex_match_id (lexer, "INCLUDEMRSETS"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (!parse_bool (lexer, &t->pairwise->include_mrsets))
+                        goto error;
+                    }
+                  else if (lex_match_id (lexer, "MEANSVARIANCE"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "ALLCATS"))
+                        t->pairwise->meansvariance_allcats = true;
+                      else if (lex_match_id (lexer, "TESTEDCATS"))
+                        t->pairwise->meansvariance_allcats = false;
+                      else
+                        {
+                          lex_error_expecting (lexer, "ALLCATS", "TESTEDCATS");
+                          goto error;
+                        }
+                    }
+                  else if (lex_match_id (lexer, "CATEGORIES"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "ALLVISIBLE"))
+                        t->pairwise->all_visible = true;
+                      else if (lex_match_id (lexer, "SUBTOTALS"))
+                        t->pairwise->all_visible = false;
+                      else
+                        {
+                          lex_error_expecting (lexer, "ALLVISIBLE",
+                                               "SUBTOTALS");
+                          goto error;
+                        }
+                    }
+                  else if (lex_match_id (lexer, "MERGE"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (!parse_bool (lexer, &t->pairwise->merge))
+                        goto error;
+                    }
+                  else if (lex_match_id (lexer, "STYLE"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (lex_match_id (lexer, "APA"))
+                        t->pairwise->apa_style = true;
+                      else if (lex_match_id (lexer, "SIMPLE"))
+                        t->pairwise->apa_style = false;
+                      else
+                        {
+                          lex_error_expecting (lexer, "APA", "SIMPLE");
+                          goto error;
+                        }
+                    }
+                  else if (lex_match_id (lexer, "SHOWSIG"))
+                    {
+                      lex_match (lexer, T_EQUALS);
+                      if (!parse_bool (lexer, &t->pairwise->show_sig))
+                        goto error;
+                    }
+                  else
+                    {
+                      lex_error_expecting (lexer, "TYPE", "ALPHA", "ADJUST",
+                                           "INCLUDEMRSETS", "MEANSVARIANCE",
+                                           "CATEGORIES", "MERGE", "STYLE",
+                                           "SHOWSIG");
+                      goto error;
+                    }
+                }
+              while (lex_token (lexer) != T_SLASH
+                     && lex_token (lexer) != T_ENDCMD);
+            }
+          else
+            {
+              lex_error_expecting (lexer, "TABLE", "SLABELS", "CLABELS",
+                                   "CRITERIA", "CATEGORIES", "TITLES",
+                                   "SIGTEST", "COMPARETEST");
+              goto error;
+            }
+        }
+    }
+  while (lex_token (lexer) != T_ENDCMD);
+
+  return CMD_SUCCESS;
 
+error:
+  /* XXX free */
+  return CMD_FAILURE;
 }