+static struct ctables_pcexpr *
+ctables_pcexpr_parse_primary (struct lexer *lexer, struct dictionary *dict)
+{
+ int start_ofs = lex_ofs (lexer);
+ struct ctables_pcexpr e;
+ if (lex_is_number (lexer))
+ {
+ e = (struct ctables_pcexpr) { .op = CTPO_CONSTANT,
+ .number = lex_number (lexer) };
+ lex_get (lexer);
+ }
+ else if (lex_match_id (lexer, "MISSING"))
+ e = (struct ctables_pcexpr) { .op = CTPO_CAT_MISSING };
+ else if (lex_match_id (lexer, "OTHERNM"))
+ e = (struct ctables_pcexpr) { .op = CTPO_CAT_OTHERNM };
+ else if (lex_match_id (lexer, "TOTAL"))
+ e = (struct ctables_pcexpr) { .op = CTPO_CAT_TOTAL };
+ else if (lex_match_id (lexer, "SUBTOTAL"))
+ {
+ size_t subtotal_index = 0;
+ if (lex_match (lexer, T_LBRACK))
+ {
+ if (!lex_force_int_range (lexer, "SUBTOTAL", 1, LONG_MAX))
+ return NULL;
+ subtotal_index = lex_integer (lexer);
+ lex_get (lexer);
+ if (!lex_force_match (lexer, T_RBRACK))
+ return NULL;
+ }
+ e = (struct ctables_pcexpr) { .op = CTPO_CAT_SUBTOTAL,
+ .subtotal_index = subtotal_index };
+ }
+ else if (lex_match (lexer, T_LBRACK))
+ {
+ if (lex_match_id (lexer, "LO"))
+ {
+ if (!lex_force_match_id (lexer, "THRU"))
+ return false;
+
+ if (lex_is_string (lexer))
+ {
+ struct substring low = { .string = NULL };
+ struct substring high = parse_substring (lexer, dict);
+ e = ctpo_cat_srange (low, high);
+ }
+ else
+ {
+ if (!lex_force_num (lexer))
+ return false;
+ e = ctpo_cat_nrange (-DBL_MAX, lex_number (lexer));
+ lex_get (lexer);
+ }
+ }
+ else if (lex_is_number (lexer))
+ {
+ double number = lex_number (lexer);
+ lex_get (lexer);
+ if (lex_match_id (lexer, "THRU"))
+ {
+ if (lex_match_id (lexer, "HI"))
+ e = ctpo_cat_nrange (number, DBL_MAX);
+ else
+ {
+ if (!lex_force_num (lexer))
+ return false;
+ e = ctpo_cat_nrange (number, lex_number (lexer));
+ lex_get (lexer);
+ }
+ }
+ else
+ e = (struct ctables_pcexpr) { .op = CTPO_CAT_NUMBER,
+ .number = number };
+ }
+ else if (lex_is_string (lexer))
+ {
+ struct substring s = parse_substring (lexer, dict);
+
+ if (lex_match_id (lexer, "THRU"))
+ {
+ struct substring high;
+
+ if (lex_match_id (lexer, "HI"))
+ high = (struct substring) { .string = NULL };
+ else
+ {
+ if (!lex_force_string (lexer))
+ {
+ ss_dealloc (&s);
+ return false;
+ }
+ high = parse_substring (lexer, dict);
+ }
+
+ e = ctpo_cat_srange (s, high);
+ }
+ else
+ e = (struct ctables_pcexpr) { .op = CTPO_CAT_STRING, .string = s };
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ return NULL;
+ }
+
+ if (!lex_force_match (lexer, T_RBRACK))
+ {
+ if (e.op == CTPO_CAT_STRING)
+ ss_dealloc (&e.string);
+ else if (e.op == CTPO_CAT_SRANGE)
+ {
+ ss_dealloc (&e.srange[0]);
+ ss_dealloc (&e.srange[1]);
+ }
+ return NULL;
+ }
+ }
+ else if (lex_match (lexer, T_LPAREN))
+ {
+ struct ctables_pcexpr *ep = ctables_pcexpr_parse_add (lexer, dict);
+ if (!ep)
+ return NULL;
+ if (!lex_force_match (lexer, T_RPAREN))
+ {
+ ctables_pcexpr_destroy (ep);
+ return NULL;
+ }
+ return ep;
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ return NULL;
+ }
+
+ e.location = lex_ofs_location (lexer, start_ofs, lex_ofs (lexer) - 1);
+ return xmemdup (&e, sizeof e);
+}
+
+static struct ctables_pcexpr *
+ctables_pcexpr_allocate_neg (struct ctables_pcexpr *sub,
+ struct lexer *lexer, int start_ofs)
+{
+ struct ctables_pcexpr *e = xmalloc (sizeof *e);
+ *e = (struct ctables_pcexpr) {
+ .op = CTPO_NEG,
+ .subs = { sub },
+ .location = lex_ofs_location (lexer, start_ofs, lex_ofs (lexer) - 1),
+ };
+ return e;
+}
+
+static struct ctables_pcexpr *
+ctables_pcexpr_parse_exp (struct lexer *lexer, struct dictionary *dict)
+{
+ static const struct operator op = { T_EXP, CTPO_POW };
+
+ const char *chain_warning =
+ _("The exponentiation operator (`**') is left-associative: "
+ "`a**b**c' equals `(a**b)**c', not `a**(b**c)'. "
+ "To disable this warning, insert parentheses.");
+
+ if (lex_token (lexer) != T_NEG_NUM || lex_next_token (lexer, 1) != T_EXP)
+ return ctables_pcexpr_parse_binary_operators (lexer, dict, &op, 1,
+ ctables_pcexpr_parse_primary,
+ chain_warning);
+
+ /* Special case for situations like "-5**6", which must be parsed as
+ -(5**6). */
+
+ int start_ofs = lex_ofs (lexer);
+ struct ctables_pcexpr *lhs = xmalloc (sizeof *lhs);
+ *lhs = (struct ctables_pcexpr) {
+ .op = CTPO_CONSTANT,
+ .number = -lex_tokval (lexer),
+ .location = lex_ofs_location (lexer, start_ofs, lex_ofs (lexer)),
+ };
+ lex_get (lexer);
+
+ struct ctables_pcexpr *node = ctables_pcexpr_parse_binary_operators__ (
+ lexer, dict, &op, 1,
+ ctables_pcexpr_parse_primary, chain_warning, lhs);
+ if (!node)
+ return NULL;
+
+ return ctables_pcexpr_allocate_neg (node, lexer, start_ofs);
+}
+
+/* Parses the unary minus level. */
+static struct ctables_pcexpr *
+ctables_pcexpr_parse_neg (struct lexer *lexer, struct dictionary *dict)
+{
+ int start_ofs = lex_ofs (lexer);
+ if (!lex_match (lexer, T_DASH))
+ return ctables_pcexpr_parse_exp (lexer, dict);
+
+ struct ctables_pcexpr *inner = ctables_pcexpr_parse_neg (lexer, dict);
+ if (!inner)
+ return NULL;
+
+ return ctables_pcexpr_allocate_neg (inner, lexer, start_ofs);
+}
+
+/* Parses the multiplication and division level. */
+static struct ctables_pcexpr *
+ctables_pcexpr_parse_mul (struct lexer *lexer, struct dictionary *dict)
+{
+ static const struct operator ops[] =
+ {
+ { T_ASTERISK, CTPO_MUL },
+ { T_SLASH, CTPO_DIV },
+ };
+
+ return ctables_pcexpr_parse_binary_operators (lexer, dict, ops,
+ sizeof ops / sizeof *ops,
+ ctables_pcexpr_parse_neg, NULL);
+}
+
+/* Parses the addition and subtraction level. */
+static struct ctables_pcexpr *
+ctables_pcexpr_parse_add (struct lexer *lexer, struct dictionary *dict)
+{
+ static const struct operator ops[] =
+ {
+ { T_PLUS, CTPO_ADD },
+ { T_DASH, CTPO_SUB },
+ { T_NEG_NUM, CTPO_ADD },
+ };
+
+ return ctables_pcexpr_parse_binary_operators (lexer, dict,
+ ops, sizeof ops / sizeof *ops,
+ ctables_pcexpr_parse_mul, NULL);
+}
+\f
+/* CTABLES axis expressions. */
+
+/* CTABLES has a number of extra formats that we implement via custom
+ currency specifications on an alternate fmt_settings. */
+#define CTEF_NEGPAREN FMT_CCA
+#define CTEF_NEQUAL FMT_CCB
+#define CTEF_PAREN FMT_CCC
+#define CTEF_PCTPAREN FMT_CCD
+
+enum ctables_summary_variant
+ {
+ CSV_CELL,
+ CSV_TOTAL
+#define N_CSVS 2
+ };
+
+struct ctables_axis
+ {
+ enum ctables_axis_op