+ struct ctables_category *cat = ctables_find_category_for_postcompute (
+ dict, cats, pc_cat->parse_format, e);
+ if (!cat)
+ {
+ if (e->op == CTPO_CAT_SUBTOTAL && e->subtotal_index == 0)
+ {
+ size_t n_subtotals = 0;
+ for (size_t i = 0; i < cats->n_cats; i++)
+ n_subtotals += cats->cats[i].type == CCT_SUBTOTAL;
+ if (n_subtotals > 1)
+ {
+ msg_at (SE, cats_location,
+ ngettext ("These categories include %zu instance "
+ "of SUBTOTAL or HSUBTOTAL, so references "
+ "from computed categories must refer to "
+ "subtotals by position, "
+ "e.g. SUBTOTAL[1].",
+ "These categories include %zu instances "
+ "of SUBTOTAL or HSUBTOTAL, so references "
+ "from computed categories must refer to "
+ "subtotals by position, "
+ "e.g. SUBTOTAL[1].",
+ n_subtotals),
+ n_subtotals);
+ msg_at (SN, e->location,
+ _("This is the reference that lacks a position."));
+ return NULL;
+ }
+ }
+
+ msg_at (SE, pc_cat->location,
+ _("Computed category &%s references a category not included "
+ "in the category list."),
+ pc_cat->pc->name);
+ msg_at (SN, e->location, _("This is the missing category."));
+ if (e->op == CTPO_CAT_SUBTOTAL)
+ msg_at (SN, cats_location,
+ _("To fix the problem, add subtotals to the "
+ "list of categories here."));
+ else if (e->op == CTPO_CAT_TOTAL)
+ msg (SN, _("To fix the problem, add TOTAL=YES to the variable's "
+ "CATEGORIES specification."));
+ else
+ msg_at (SN, cats_location,
+ _("To fix the problem, add the missing category to the "
+ "list of categories here."));
+ return false;
+ }
+ if (pc_cat->pc->hide_source_cats)
+ cat->hide = true;
+ return true;
+ }
+
+ case CTPO_CONSTANT:
+ return true;
+
+ case CTPO_ADD:
+ case CTPO_SUB:
+ case CTPO_MUL:
+ case CTPO_DIV:
+ case CTPO_POW:
+ case CTPO_NEG:
+ for (size_t i = 0; i < 2; i++)
+ if (e->subs[i] && !ctables_recursive_check_postcompute (
+ dict, e->subs[i], pc_cat, cats, cats_location))
+ return false;
+ return true;
+ }
+
+ NOT_REACHED ();
+}
+
+static struct pivot_value *
+ctables_postcompute_label (const struct ctables_categories *cats,
+ const struct ctables_category *cat,
+ const struct variable *var)
+{
+ struct substring in = ss_cstr (cat->pc->label);
+ struct substring target = ss_cstr (")LABEL[");
+
+ struct string out = DS_EMPTY_INITIALIZER;
+ for (;;)
+ {
+ size_t chunk = ss_find_substring (in, target);
+ if (chunk == SIZE_MAX)
+ {
+ if (ds_is_empty (&out))
+ return pivot_value_new_user_text (in.string, in.length);
+ else
+ {
+ ds_put_substring (&out, in);
+ return pivot_value_new_user_text_nocopy (ds_steal_cstr (&out));
+ }
+ }
+
+ ds_put_substring (&out, ss_head (in, chunk));
+ ss_advance (&in, chunk + target.length);
+
+ struct substring idx_s;
+ if (!ss_get_until (&in, ']', &idx_s))
+ goto error;
+ char *tail;
+ long int idx = strtol (idx_s.string, &tail, 10);
+ if (idx < 1 || idx > cats->n_cats || tail != ss_end (idx_s))
+ goto error;
+
+ struct ctables_category *cat2 = &cats->cats[idx - 1];
+ if (!ctables_category_format_label (cat2, var, &out))
+ goto error;
+ }
+
+error:
+ ds_destroy (&out);
+ return pivot_value_new_user_text (cat->pc->label, SIZE_MAX);
+}
+
+static struct pivot_value *
+ctables_category_create_value_label (const struct ctables_categories *cats,
+ const struct ctables_category *cat,
+ const struct variable *var,
+ const union value *value)
+{
+ return (cat->type == CCT_POSTCOMPUTE && cat->pc->label
+ ? ctables_postcompute_label (cats, cat, var)
+ : cat->type == CCT_TOTAL || cat->type == CCT_SUBTOTAL
+ ? pivot_value_new_user_text (cat->total_label, SIZE_MAX)
+ : pivot_value_new_var_value (var, value));
+}
+\f
+/* CTABLES variable nesting and stacking. */
+
+/* A nested sequence of variables, e.g. a > b > c. */
+struct ctables_nest
+ {
+ struct variable **vars;
+ size_t n;
+ size_t scale_idx;
+ size_t summary_idx;
+ size_t *areas[N_CTATS];
+ size_t n_areas[N_CTATS];
+ size_t group_head;
+
+ struct ctables_summary_spec_set specs[N_CSVS];
+ };
+
+/* A stack of nestings, e.g. nest1 + nest2 + ... + nestN. */
+struct ctables_stack
+ {
+ struct ctables_nest *nests;
+ size_t n;
+ };
+
+static void
+ctables_nest_uninit (struct ctables_nest *nest)
+{
+ free (nest->vars);
+ for (enum ctables_summary_variant sv = 0; sv < N_CSVS; sv++)
+ ctables_summary_spec_set_uninit (&nest->specs[sv]);
+ for (enum ctables_area_type at = 0; at < N_CTATS; at++)
+ free (nest->areas[at]);
+}
+
+static void
+ctables_stack_uninit (struct ctables_stack *stack)
+{
+ if (stack)
+ {
+ for (size_t i = 0; i < stack->n; i++)
+ ctables_nest_uninit (&stack->nests[i]);
+ free (stack->nests);
+ }
+}
+
+static struct ctables_stack
+nest_fts (struct ctables_stack s0, struct ctables_stack s1)
+{
+ if (!s0.n)
+ return s1;
+ else if (!s1.n)
+ return s0;
+
+ struct ctables_stack stack = { .nests = xnmalloc (s0.n, s1.n * sizeof *stack.nests) };
+ for (size_t i = 0; i < s0.n; i++)
+ for (size_t j = 0; j < s1.n; j++)
+ {
+ const struct ctables_nest *a = &s0.nests[i];
+ const struct ctables_nest *b = &s1.nests[j];
+
+ size_t allocate = a->n + b->n;
+ struct variable **vars = xnmalloc (allocate, sizeof *vars);
+ size_t n = 0;
+ for (size_t k = 0; k < a->n; k++)
+ vars[n++] = a->vars[k];
+ for (size_t k = 0; k < b->n; k++)
+ vars[n++] = b->vars[k];
+ assert (n == allocate);
+
+ const struct ctables_nest *summary_src;
+ if (!a->specs[CSV_CELL].var)
+ summary_src = b;
+ else if (!b->specs[CSV_CELL].var)
+ summary_src = a;
+ else
+ NOT_REACHED ();
+
+ struct ctables_nest *new = &stack.nests[stack.n++];
+ *new = (struct ctables_nest) {
+ .vars = vars,
+ .scale_idx = (a->scale_idx != SIZE_MAX ? a->scale_idx