dictionary: Limit split file variables to 8, for compatibility.
[pspp] / src / data / dictionary.c
index 7379d1940ad9b511bcffa03faf301aa0f7727238..c13321c7827bb2cd877c598d834f5bbc2a341c6e 100644 (file)
@@ -1,5 +1,6 @@
 /* PSPP - a program for statistical analysis.
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 1997-9, 2000, 2006, 2007, 2009 Free Software Foundation, Inc.
+   Copyright (C) 1997-9, 2000, 2006, 2007, 2009, 2010, 2011, 2012, 2013, 2014,
+   2015, 2020 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
 
 #include <config.h>
 
 
 #include <config.h>
 
-#include "dictionary.h"
+#include "data/dictionary.h"
 
 
+#include <stdint.h>
 #include <stdlib.h>
 #include <ctype.h>
 #include <stdlib.h>
 #include <ctype.h>
-
-#include <data/attributes.h>
-#include <data/case.h>
-#include <data/category.h>
-#include <data/identifier.h>
-#include <data/settings.h>
-#include <data/value-labels.h>
-#include <data/vardict.h>
-#include <data/variable.h>
-#include <data/vector.h>
-#include <libpspp/array.h>
-#include <libpspp/assertion.h>
-#include <libpspp/compiler.h>
-#include <libpspp/hash.h>
-#include <libpspp/message.h>
-#include <libpspp/misc.h>
-#include <libpspp/pool.h>
-#include <libpspp/str.h>
-
-#include "intprops.h"
-#include "minmax.h"
-#include "xalloc.h"
+#include <unistr.h>
+
+#include "data/attributes.h"
+#include "data/case.h"
+#include "data/identifier.h"
+#include "data/mrset.h"
+#include "data/settings.h"
+#include "data/value-labels.h"
+#include "data/vardict.h"
+#include "data/variable.h"
+#include "data/vector.h"
+#include "libpspp/array.h"
+#include "libpspp/assertion.h"
+#include "libpspp/compiler.h"
+#include "libpspp/hash-functions.h"
+#include "libpspp/hmap.h"
+#include "libpspp/i18n.h"
+#include "libpspp/message.h"
+#include "libpspp/misc.h"
+#include "libpspp/pool.h"
+#include "libpspp/str.h"
+#include "libpspp/string-array.h"
+#include "libpspp/ll.h"
+
+#include "gl/intprops.h"
+#include "gl/minmax.h"
+#include "gl/xalloc.h"
+#include "gl/xmemdup0.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 /* A dictionary. */
 struct dictionary
   {
 /* A dictionary. */
 struct dictionary
   {
-    struct variable **var;     /* Variables. */
-    size_t var_cnt, var_cap;    /* Number of variables, capacity. */
+    int ref_cnt;
+    struct vardict_info *vars; /* Variables. */
+    size_t n_vars;              /* Number of variables. */
+    size_t allocated_vars;      /* Allocated space in 'vars'. */
     struct caseproto *proto;    /* Prototype for dictionary cases
                                    (updated lazily). */
     struct caseproto *proto;    /* Prototype for dictionary cases
                                    (updated lazily). */
-    struct hsh_table *name_tab;        /* Variable index by name. */
+    struct hmap name_map;      /* Variable index by name. */
     int next_value_idx;         /* Index of next `union value' to allocate. */
     const struct variable **split;    /* SPLIT FILE vars. */
     int next_value_idx;         /* Index of next `union value' to allocate. */
     const struct variable **split;    /* SPLIT FILE vars. */
-    size_t split_cnt;           /* SPLIT FILE count. */
+    size_t n_splits;            /* SPLIT FILE count. */
+    enum split_type split_type;
     struct variable *weight;    /* WEIGHT variable. */
     struct variable *filter;    /* FILTER variable. */
     casenumber case_limit;      /* Current case limit (N command). */
     char *label;               /* File label. */
     struct variable *weight;    /* WEIGHT variable. */
     struct variable *filter;    /* FILTER variable. */
     casenumber case_limit;      /* Current case limit (N command). */
     char *label;               /* File label. */
-    struct string documents;    /* Documents, as a string. */
+    struct string_array documents; /* Documents. */
     struct vector **vector;     /* Vectors of variables. */
     struct vector **vector;     /* Vectors of variables. */
-    size_t vector_cnt;          /* Number of vectors. */
+    size_t n_vectors;           /* Number of vectors. */
     struct attrset attributes;  /* Custom attributes. */
     struct attrset attributes;  /* Custom attributes. */
+    struct mrset **mrsets;      /* Multiple response sets. */
+    size_t n_mrsets;            /* Number of multiple response sets. */
+
+    /* Whether variable names must be valid identifiers.  Normally, this is
+       true, but sometimes a dictionary is prepared for external use
+       (e.g. output to a CSV file) where names don't have to be valid. */
+    bool names_must_be_ids;
 
     char *encoding;             /* Character encoding of string data */
 
 
     char *encoding;             /* Character encoding of string data */
 
@@ -76,20 +94,101 @@ struct dictionary
     void *changed_data;
   };
 
     void *changed_data;
   };
 
+static void dict_unset_split_var (struct dictionary *, struct variable *, bool);
+static void dict_unset_mrset_var (struct dictionary *, struct variable *);
 
 
-void
-dict_set_encoding (struct dictionary *d, const char *enc)
+/* Compares two double pointers to variables, which should point
+   to elements of a struct dictionary's `var' member array. */
+static int
+compare_var_ptrs (const void *a_, const void *b_, const void *aux UNUSED)
+{
+  struct variable *const *a = a_;
+  struct variable *const *b = b_;
+
+  return *a < *b ? -1 : *a > *b;
+}
+
+static void
+unindex_var (struct dictionary *d, struct vardict_info *vardict)
+{
+  hmap_delete (&d->name_map, &vardict->name_node);
+}
+
+/* This function assumes that vardict->name_node.hash is valid, that is, that
+   its name has not changed since it was hashed (rename_var() updates this
+   hash along with the name itself). */
+static void
+reindex_var (struct dictionary *d, struct vardict_info *vardict, bool skip_callbacks)
+{
+  struct variable *old = (d->callbacks && d->callbacks->var_changed
+                          ? var_clone (vardict->var)
+                          : NULL);
+
+  struct variable *var = vardict->var;
+  var_set_vardict (var, vardict);
+  hmap_insert_fast (&d->name_map, &vardict->name_node,
+                    vardict->name_node.hash);
+
+  if (! skip_callbacks)
+    {
+      if (d->changed) d->changed (d, d->changed_data);
+      if (old)
+        {
+          d->callbacks->var_changed (d, var_get_dict_index (var), VAR_TRAIT_POSITION, old, d->cb_data);
+          var_unref (old);
+        }
+    }
+}
+
+/* Sets the case_index in V's vardict to CASE_INDEX. */
+static void
+set_var_case_index (struct variable *v, int case_index)
+{
+  var_get_vardict (v)->case_index = case_index;
+}
+
+/* Removes the dictionary variables with indexes from FROM to TO (exclusive)
+   from name_map. */
+static void
+unindex_vars (struct dictionary *d, size_t from, size_t to)
+{
+  size_t i;
+
+  for (i = from; i < to; i++)
+    unindex_var (d, &d->vars[i]);
+}
+
+/* Re-sets the dict_index in the dictionary variables with
+   indexes from FROM to TO (exclusive). */
+static void
+reindex_vars (struct dictionary *d, size_t from, size_t to, bool skip_callbacks)
 {
 {
-  if (enc)
-    d->encoding = xstrdup (enc);
+  size_t i;
+
+  for (i = from; i < to; i++)
+    reindex_var (d, &d->vars[i], skip_callbacks);
 }
 
 }
 
+\f
+
+/* Returns the encoding for data in dictionary D.  The return value is a
+   nonnull string that contains an IANA character set name. */
 const char *
 dict_get_encoding (const struct dictionary *d)
 {
   return d->encoding ;
 }
 
 const char *
 dict_get_encoding (const struct dictionary *d)
 {
   return d->encoding ;
 }
 
+/* Returns true if UTF-8 string ID is an acceptable identifier in DICT's
+   encoding, false otherwise.  If ISSUE_ERROR is true, issues an explanatory
+   error message on failure. */
+bool
+dict_id_is_valid (const struct dictionary *dict, const char *id,
+                  bool issue_error)
+{
+  return (!dict->names_must_be_ids
+          || id_is_valid (id, dict->encoding, issue_error));
+}
 
 void
 dict_set_change_callback (struct dictionary *d,
 
 void
 dict_set_change_callback (struct dictionary *d,
@@ -115,11 +214,10 @@ void
 dict_dump (const struct dictionary *d)
 {
   int i;
 dict_dump (const struct dictionary *d)
 {
   int i;
-  for (i = 0 ; i < d->var_cnt ; ++i )
+  for (i = 0 ; i < d->n_vars ; ++i)
     {
     {
-      const struct variable *v =
-       d->var[i];
-      printf ("Name: %s;\tdict_idx: %d; case_idx: %d\n",
+      const struct variable *v = d->vars[i].var;
+      printf ("Name: %s;\tdict_idx: %zu; case_idx: %zu\n",
              var_get_name (v),
              var_get_dict_index (v),
              var_get_case_index (v));
              var_get_name (v),
              var_get_dict_index (v),
              var_get_case_index (v));
@@ -150,15 +248,21 @@ dict_copy_callbacks (struct dictionary *dest,
   dest->cb_data = src->cb_data;
 }
 
   dest->cb_data = src->cb_data;
 }
 
-/* Creates and returns a new dictionary. */
+/* Creates and returns a new dictionary with the specified ENCODING. */
 struct dictionary *
 struct dictionary *
-dict_create (void)
+dict_create (const char *encoding)
 {
 {
-  struct dictionary *d = xzalloc (sizeof *d);
+  struct dictionary *d = xmalloc (sizeof *d);
+
+  *d = (struct dictionary) {
+    .encoding = xstrdup (encoding),
+    .names_must_be_ids = true,
+    .name_map = HMAP_INITIALIZER (d->name_map),
+    .attributes = ATTRSET_INITIALIZER (d->attributes),
+    .split_type = SPLIT_NONE,
+    .ref_cnt = 1,
+  };
 
 
-  d->name_tab = hsh_create (8, compare_vars_by_name, hash_var_by_name,
-                            NULL, NULL);
-  attrset_init (&d->attributes);
   return d;
 }
 
   return d;
 }
 
@@ -169,43 +273,40 @@ dict_create (void)
    dictionary.  If the new dictionary won't be used to access
    cases produced with the old dictionary, then the new
    dictionary's case indexes should be compacted with
    dictionary.  If the new dictionary won't be used to access
    cases produced with the old dictionary, then the new
    dictionary's case indexes should be compacted with
-   dict_compact_values to save space. */
+   dict_compact_values to save space.
+
+   Callbacks are not cloned. */
 struct dictionary *
 dict_clone (const struct dictionary *s)
 {
   struct dictionary *d;
   size_t i;
 
 struct dictionary *
 dict_clone (const struct dictionary *s)
 {
   struct dictionary *d;
   size_t i;
 
-  assert (s != NULL);
+  d = dict_create (s->encoding);
+  dict_set_names_must_be_ids (d, dict_get_names_must_be_ids (s));
 
 
-  d = dict_create ();
-
-  for (i = 0; i < s->var_cnt; i++)
+  for (i = 0; i < s->n_vars; i++)
     {
     {
-      const struct vardict_info *svdi;
-      struct vardict_info dvdi;
-      struct variable *sv = s->var[i];
-      struct variable *dv = dict_clone_var_assert (d, sv, var_get_name (sv));
+      struct variable *sv = s->vars[i].var;
+      struct variable *dv = dict_clone_var_assert (d, sv);
       size_t i;
 
       size_t i;
 
-      for (i = 0; i < var_get_short_name_cnt (sv); i++)
+      for (i = 0; i < var_get_n_short_names (sv); i++)
         var_set_short_name (dv, i, var_get_short_name (sv, i));
 
         var_set_short_name (dv, i, var_get_short_name (sv, i));
 
-      svdi = var_get_vardict (sv);
-      dvdi = *svdi;
-      dvdi.dict = d;
-      var_set_vardict (dv, &dvdi);
+      var_get_vardict (dv)->case_index = var_get_vardict (sv)->case_index;
     }
 
   d->next_value_idx = s->next_value_idx;
 
     }
 
   d->next_value_idx = s->next_value_idx;
 
-  d->split_cnt = s->split_cnt;
-  if (d->split_cnt > 0)
+  d->n_splits = s->n_splits;
+  if (d->n_splits > 0)
     {
     {
-      d->split = xnmalloc (d->split_cnt, sizeof *d->split);
-      for (i = 0; i < d->split_cnt; i++)
-        d->split[i] = dict_lookup_var_assert (d, var_get_name (s->split[i]));
+       d->split = xnmalloc (d->n_splits, sizeof *d->split);
+       for (i = 0; i < d->n_splits; i++)
+         d->split[i] = dict_lookup_var_assert (d, var_get_name (s->split[i]));
     }
     }
+  d->split_type = s->split_type;
 
   if (s->weight != NULL)
     dict_set_weight (d, dict_lookup_var_assert (d, var_get_name (s->weight)));
 
   if (s->weight != NULL)
     dict_set_weight (d, dict_lookup_var_assert (d, var_get_name (s->weight)));
@@ -217,178 +318,487 @@ dict_clone (const struct dictionary *s)
   dict_set_label (d, dict_get_label (s));
   dict_set_documents (d, dict_get_documents (s));
 
   dict_set_label (d, dict_get_label (s));
   dict_set_documents (d, dict_get_documents (s));
 
-  d->vector_cnt = s->vector_cnt;
-  d->vector = xnmalloc (d->vector_cnt, sizeof *d->vector);
-  for (i = 0; i < s->vector_cnt; i++)
+  d->n_vectors = s->n_vectors;
+  d->vector = xnmalloc (d->n_vectors, sizeof *d->vector);
+  for (i = 0; i < s->n_vectors; i++)
     d->vector[i] = vector_clone (s->vector[i], s, d);
 
     d->vector[i] = vector_clone (s->vector[i], s, d);
 
-  if ( s->encoding)
-    d->encoding = xstrdup (s->encoding);
-
   dict_set_attributes (d, dict_get_attributes (s));
 
   dict_set_attributes (d, dict_get_attributes (s));
 
+  for (i = 0; i < s->n_mrsets; i++)
+    {
+      const struct mrset *old = s->mrsets[i];
+      struct mrset *new;
+      size_t j;
+
+      /* Clone old mrset, then replace vars from D by vars from S. */
+      new = mrset_clone (old);
+      for (j = 0; j < new->n_vars; j++)
+        new->vars[j] = dict_lookup_var_assert (d, var_get_name (new->vars[j]));
+
+      dict_add_mrset (d, new);
+    }
+
   return d;
 }
 
   return d;
 }
 
+
+\f
+/* Returns the SPLIT FILE vars (see cmd_split_file()).  Call
+   dict_get_n_splits() to determine how many SPLIT FILE vars
+   there are.  Returns a null pointer if and only if there are no
+   SPLIT FILE vars. */
+const struct variable *const *
+dict_get_split_vars (const struct dictionary *d)
+{
+  return d->split;
+}
+
+/* Returns the number of SPLIT FILE vars. */
+size_t
+dict_get_n_splits (const struct dictionary *d)
+{
+  return d->n_splits;
+}
+
+/* Removes variable V, which must be in D, from D's set of split
+   variables. */
+static void
+dict_unset_split_var (struct dictionary *d, struct variable *v, bool skip_callbacks)
+{
+  int orig_count;
+
+  assert (dict_contains_var (d, v));
+
+  orig_count = d->n_splits;
+  d->n_splits = remove_equal (d->split, d->n_splits, sizeof *d->split,
+                               &v, compare_var_ptrs, NULL);
+  if (orig_count != d->n_splits && !skip_callbacks)
+    {
+      if (d->changed) d->changed (d, d->changed_data);
+      /* We changed the set of split variables so invoke the
+         callback. */
+      if (d->callbacks &&  d->callbacks->split_changed)
+        d->callbacks->split_changed (d, d->cb_data);
+    }
+}
+
+
+/* Sets N split vars SPLIT in dictionary D.  N is silently capped to a maximum
+   of MAX_SPLITS. */
+static void
+dict_set_split_vars__ (struct dictionary *d,
+                       struct variable *const *split, size_t n,
+                       enum split_type type, bool skip_callbacks)
+{
+  if (n > MAX_SPLITS)
+    n = MAX_SPLITS;
+  assert (n == 0 || split != NULL);
+
+  d->n_splits = n;
+  d->split_type = type == SPLIT_NONE ? SPLIT_LAYERED : type;
+  if (n > 0)
+   {
+    d->split = xnrealloc (d->split, n, sizeof *d->split) ;
+    memcpy (d->split, split, n * sizeof *d->split);
+   }
+  else
+   {
+    free (d->split);
+    d->split = NULL;
+   }
+
+ if (!skip_callbacks)
+    {
+      if (d->changed) d->changed (d, d->changed_data);
+      if (d->callbacks &&  d->callbacks->split_changed)
+        d->callbacks->split_changed (d, d->cb_data);
+    }
+}
+
+/* Sets N split vars SPLIT in dictionary D. */
+void
+dict_set_split_vars (struct dictionary *d,
+                     struct variable *const *split, size_t n,
+                     enum split_type type)
+{
+  dict_set_split_vars__ (d, split, n, type, false);
+}
+
+void
+dict_clear_split_vars (struct dictionary *d)
+{
+  dict_set_split_vars (d, NULL, 0, SPLIT_NONE);
+}
+\f
+
+/* Deletes variable V from dictionary D and frees V.
+
+   This is a very bad idea if there might be any pointers to V
+   from outside D.  In general, no variable in the active dataset's
+   dictionary should be deleted when any transformations are
+   active on the dictionary's dataset, because those
+   transformations might reference the deleted variable.  The
+   safest time to delete a variable is just after a procedure has
+   been executed, as done by DELETE VARIABLES.
+
+   Pointers to V within D are not a problem, because
+   dict_delete_var() knows to remove V from split variables,
+   weights, filters, etc. */
+static void
+dict_delete_var__ (struct dictionary *d, struct variable *v, bool skip_callbacks)
+{
+  int dict_index = var_get_dict_index (v);
+  const int case_index = var_get_case_index (v);
+
+  assert (dict_contains_var (d, v));
+
+  dict_unset_split_var (d, v, skip_callbacks);
+  dict_unset_mrset_var (d, v);
+
+  if (d->weight == v)
+    dict_set_weight (d, NULL);
+
+  if (d->filter == v)
+    dict_set_filter (d, NULL);
+
+  dict_clear_vectors (d);
+
+  /* Remove V from var array. */
+  unindex_vars (d, dict_index, d->n_vars);
+  remove_element (d->vars, d->n_vars, sizeof *d->vars, dict_index);
+  d->n_vars--;
+
+  /* Update dict_index for each affected variable. */
+  reindex_vars (d, dict_index, d->n_vars, skip_callbacks);
+
+  /* Free memory. */
+  var_clear_vardict (v);
+
+  if (! skip_callbacks)
+    {
+      if (d->changed) d->changed (d, d->changed_data);
+      if (d->callbacks &&  d->callbacks->var_deleted)
+        d->callbacks->var_deleted (d, v, dict_index, case_index, d->cb_data);
+    }
+
+  invalidate_proto (d);
+  var_unref (v);
+}
+
+/* Deletes variable V from dictionary D and frees V.
+
+   This is a very bad idea if there might be any pointers to V
+   from outside D.  In general, no variable in the active dataset's
+   dictionary should be deleted when any transformations are
+   active on the dictionary's dataset, because those
+   transformations might reference the deleted variable.  The
+   safest time to delete a variable is just after a procedure has
+   been executed, as done by DELETE VARIABLES.
+
+   Pointers to V within D are not a problem, because
+   dict_delete_var() knows to remove V from split variables,
+   weights, filters, etc. */
+void
+dict_delete_var (struct dictionary *d, struct variable *v)
+{
+  dict_delete_var__ (d, v, false);
+}
+
+
+/* Deletes the COUNT variables listed in VARS from D.  This is
+   unsafe; see the comment on dict_delete_var() for details. */
+void
+dict_delete_vars (struct dictionary *d,
+                  struct variable *const *vars, size_t count)
+{
+  /* FIXME: this can be done in O(count) time, but this algorithm
+     is O(count**2). */
+  assert (count == 0 || vars != NULL);
+
+  while (count-- > 0)
+    dict_delete_var (d, *vars++);
+}
+
+/* Deletes the COUNT variables in D starting at index IDX.  This
+   is unsafe; see the comment on dict_delete_var() for
+   details. Deleting consecutive vars will result in less callbacks
+   compared to iterating over dict_delete_var.
+   A simple while loop over dict_delete_var will
+   produce (d->n_vars - IDX) * COUNT variable changed callbacks
+   plus COUNT variable delete callbacks.
+   This here produces d->n_vars - IDX variable changed callbacks
+   plus COUNT variable delete callbacks. */
+void
+dict_delete_consecutive_vars (struct dictionary *d, size_t idx, size_t count)
+{
+  assert (idx + count <= d->n_vars);
+
+  /* We need to store the variable and the corresponding case_index
+     for the delete callbacks later. We store them in a linked list.*/
+  struct delvar {
+    struct ll ll;
+    struct variable *var;
+    int case_index;
+  };
+  struct ll_list list = LL_INITIALIZER (list);
+
+  for (size_t i = idx; i < idx + count; i++)
+    {
+      struct delvar *dv = xmalloc (sizeof (struct delvar));
+      assert (dv);
+      struct variable *v = d->vars[i].var;
+
+      dict_unset_split_var (d, v, false);
+      dict_unset_mrset_var (d, v);
+
+      if (d->weight == v)
+       dict_set_weight (d, NULL);
+
+      if (d->filter == v)
+       dict_set_filter (d, NULL);
+
+      dv->var = v;
+      dv->case_index = var_get_case_index (v);
+      ll_push_tail (&list, (struct ll *)dv);
+    }
+
+  dict_clear_vectors (d);
+
+  /* Remove variables from var array. */
+  unindex_vars (d, idx, d->n_vars);
+  remove_range (d->vars, d->n_vars, sizeof *d->vars, idx, count);
+  d->n_vars -= count;
+
+  /* Reindexing will result variable-changed callback */
+  reindex_vars (d, idx, d->n_vars, false);
+
+  invalidate_proto (d);
+  if (d->changed) d->changed (d, d->changed_data);
+
+  /* Now issue the variable delete callbacks and delete
+     the variables. The vardict is not valid at this point
+     anymore. That is the reason why we stored the
+     caseindex before reindexing. */
+  for (size_t vi = idx; vi < idx + count; vi++)
+    {
+      struct delvar *dv = (struct delvar *) ll_pop_head (&list);
+      var_clear_vardict (dv->var);
+      if (d->callbacks &&  d->callbacks->var_deleted)
+        d->callbacks->var_deleted (d, dv->var, vi, dv->case_index, d->cb_data);
+      var_unref (dv->var);
+      free (dv);
+    }
+}
+
+/* Deletes scratch variables from dictionary D. */
+void
+dict_delete_scratch_vars (struct dictionary *d)
+{
+  int i;
+
+  /* FIXME: this can be done in O(count) time, but this algorithm
+     is O(count**2). */
+  for (i = 0; i < d->n_vars;)
+    if (var_get_dict_class (d->vars[i].var) == DC_SCRATCH)
+      dict_delete_var (d, d->vars[i].var);
+    else
+      i++;
+}
+
+\f
+
 /* Clears the contents from a dictionary without destroying the
    dictionary itself. */
 /* Clears the contents from a dictionary without destroying the
    dictionary itself. */
-void
-dict_clear (struct dictionary *d)
+static void
+dict_clear__ (struct dictionary *d, bool skip_callbacks)
 {
   /* FIXME?  Should we really clear case_limit, label, documents?
      Others are necessarily cleared by deleting all the variables.*/
 {
   /* FIXME?  Should we really clear case_limit, label, documents?
      Others are necessarily cleared by deleting all the variables.*/
-  assert (d != NULL);
-
-  while (d->var_cnt > 0 )
+  while (d->n_vars > 0)
     {
     {
-      dict_delete_var (d, d->var[d->var_cnt - 1]);
+      dict_delete_var__ (d, d->vars[d->n_vars - 1].var, skip_callbacks);
     }
 
     }
 
-  free (d->var);
-  d->var = NULL;
-  d->var_cnt = d->var_cap = 0;
+  free (d->vars);
+  d->vars = NULL;
+  d->n_vars = d->allocated_vars = 0;
   invalidate_proto (d);
   invalidate_proto (d);
-  hsh_clear (d->name_tab);
+  hmap_clear (&d->name_map);
   d->next_value_idx = 0;
   d->next_value_idx = 0;
-  dict_set_split_vars (d, NULL, 0);
-  dict_set_weight (d, NULL);
-  dict_set_filter (d, NULL);
+  dict_set_split_vars__ (d, NULL, 0, SPLIT_NONE, skip_callbacks);
+
+  if (skip_callbacks)
+    {
+      d->weight = NULL;
+      d->filter = NULL;
+    }
+  else
+    {
+      dict_set_weight (d, NULL);
+      dict_set_filter (d, NULL);
+    }
   d->case_limit = 0;
   free (d->label);
   d->label = NULL;
   d->case_limit = 0;
   free (d->label);
   d->label = NULL;
-  ds_destroy (&d->documents);
+  string_array_clear (&d->documents);
   dict_clear_vectors (d);
   attrset_clear (&d->attributes);
 }
 
   dict_clear_vectors (d);
   attrset_clear (&d->attributes);
 }
 
-/* Destroys the aux data for every variable in D, by calling
-   var_clear_aux() for each variable. */
+/* Clears the contents from a dictionary without destroying the
+   dictionary itself. */
 void
 void
-dict_clear_aux (struct dictionary *d)
+dict_clear (struct dictionary *d)
 {
 {
-  int i;
+  dict_clear__ (d, false);
+}
 
 
-  assert (d != NULL);
+/* Clears a dictionary and destroys it. */
+static void
+_dict_destroy (struct dictionary *d)
+{
+  /* In general, we don't want callbacks occurring, if the dictionary
+     is being destroyed */
+  d->callbacks  = NULL ;
 
 
-  for (i = 0; i < d->var_cnt; i++)
-    var_clear_aux (d->var[i]);
+  dict_clear__ (d, true);
+  string_array_destroy (&d->documents);
+  hmap_destroy (&d->name_map);
+  attrset_destroy (&d->attributes);
+  dict_clear_mrsets (d);
+  free (d->encoding);
+  free (d);
+}
+
+struct dictionary *
+dict_ref (struct dictionary *d)
+{
+  d->ref_cnt++;
+  return d;
 }
 
 }
 
-/* Clears a dictionary and destroys it. */
 void
 void
-dict_destroy (struct dictionary *d)
+dict_unref (struct dictionary *d)
 {
 {
-  if (d != NULL)
-    {
-      /* In general, we don't want callbacks occuring, if the dictionary
-        is being destroyed */
-      d->callbacks  = NULL ;
-
-      dict_clear (d);
-      hsh_destroy (d->name_tab);
-      attrset_destroy (&d->attributes);
-      free (d);
-    }
+  if (d == NULL)
+    return;
+  d->ref_cnt--;
+  assert (d->ref_cnt >= 0);
+  if (d->ref_cnt == 0)
+    _dict_destroy (d);
 }
 
 /* Returns the number of variables in D. */
 size_t
 }
 
 /* Returns the number of variables in D. */
 size_t
-dict_get_var_cnt (const struct dictionary *d)
+dict_get_n_vars (const struct dictionary *d)
 {
 {
-  assert (d != NULL);
-
-  return d->var_cnt;
+  return d->n_vars;
 }
 
 /* Returns the variable in D with dictionary index IDX, which
    must be between 0 and the count returned by
 }
 
 /* Returns the variable in D with dictionary index IDX, which
    must be between 0 and the count returned by
-   dict_get_var_cnt(), exclusive. */
+   dict_get_n_vars(), exclusive. */
 struct variable *
 dict_get_var (const struct dictionary *d, size_t idx)
 {
 struct variable *
 dict_get_var (const struct dictionary *d, size_t idx)
 {
-  assert (d != NULL);
-  assert (idx < d->var_cnt);
+  assert (idx < d->n_vars);
 
 
-  return d->var[idx];
+  return d->vars[idx].var;
 }
 
 }
 
-/* Sets *VARS to an array of pointers to variables in D and *CNT
+/* Sets *VARS to an array of pointers to variables in D and *N
    to the number of variables in *D.  All variables are returned
    except for those, if any, in the classes indicated by EXCLUDE.
    (There is no point in putting DC_SYSTEM in EXCLUDE as
    dictionaries never include system variables.) */
 void
 dict_get_vars (const struct dictionary *d, const struct variable ***vars,
    to the number of variables in *D.  All variables are returned
    except for those, if any, in the classes indicated by EXCLUDE.
    (There is no point in putting DC_SYSTEM in EXCLUDE as
    dictionaries never include system variables.) */
 void
 dict_get_vars (const struct dictionary *d, const struct variable ***vars,
-               size_t *cnt, enum dict_class exclude)
+               size_t *n, enum dict_class exclude)
 {
 {
-  dict_get_vars_mutable (d, (struct variable ***) vars, cnt, exclude);
+  dict_get_vars_mutable (d, (struct variable ***) vars, n, exclude);
 }
 
 }
 
-/* Sets *VARS to an array of pointers to variables in D and *CNT
+/* Sets *VARS to an array of pointers to variables in D and *N
    to the number of variables in *D.  All variables are returned
    except for those, if any, in the classes indicated by EXCLUDE.
    (There is no point in putting DC_SYSTEM in EXCLUDE as
    dictionaries never include system variables.) */
 void
 dict_get_vars_mutable (const struct dictionary *d, struct variable ***vars,
    to the number of variables in *D.  All variables are returned
    except for those, if any, in the classes indicated by EXCLUDE.
    (There is no point in putting DC_SYSTEM in EXCLUDE as
    dictionaries never include system variables.) */
 void
 dict_get_vars_mutable (const struct dictionary *d, struct variable ***vars,
-                       size_t *cnt, enum dict_class exclude)
+                       size_t *n, enum dict_class exclude)
 {
   size_t count;
   size_t i;
 
 {
   size_t count;
   size_t i;
 
-  assert (d != NULL);
-  assert (vars != NULL);
-  assert (cnt != NULL);
   assert (exclude == (exclude & DC_ALL));
 
   count = 0;
   assert (exclude == (exclude & DC_ALL));
 
   count = 0;
-  for (i = 0; i < d->var_cnt; i++)
+  for (i = 0; i < d->n_vars; i++)
     {
     {
-      enum dict_class class = var_get_dict_class (d->var[i]);
+      enum dict_class class = var_get_dict_class (d->vars[i].var);
       if (!(class & exclude))
         count++;
     }
 
   *vars = xnmalloc (count, sizeof **vars);
       if (!(class & exclude))
         count++;
     }
 
   *vars = xnmalloc (count, sizeof **vars);
-  *cnt = 0;
-  for (i = 0; i < d->var_cnt; i++)
+  *n = 0;
+  for (i = 0; i < d->n_vars; i++)
     {
     {
-      enum dict_class class = var_get_dict_class (d->var[i]);
+      enum dict_class class = var_get_dict_class (d->vars[i].var);
       if (!(class & exclude))
       if (!(class & exclude))
-        (*vars)[(*cnt)++] = d->var[i];
+        (*vars)[(*n)++] = d->vars[i].var;
     }
     }
-  assert (*cnt == count);
+  assert (*n == count);
 }
 
 static struct variable *
 }
 
 static struct variable *
-add_var (struct dictionary *d, struct variable *v)
+add_var_with_case_index (struct dictionary *d, struct variable *v,
+                         int case_index)
 {
 {
-  /* Add dictionary info to variable. */
-  struct vardict_info vdi;
-  vdi.case_index = d->next_value_idx;
-  vdi.dict_index = d->var_cnt;
-  vdi.dict = d;
-  var_set_vardict (v, &vdi);
+  struct vardict_info *vardict;
+
+  assert (case_index >= d->next_value_idx);
 
   /* Update dictionary. */
 
   /* Update dictionary. */
-  if (d->var_cnt >= d->var_cap)
+  if (d->n_vars >= d->allocated_vars)
     {
     {
-      d->var_cap = 8 + 2 * d->var_cap;
-      d->var = xnrealloc (d->var, d->var_cap, sizeof *d->var);
+      size_t i;
+
+      d->vars = x2nrealloc (d->vars, &d->allocated_vars, sizeof *d->vars);
+      hmap_clear (&d->name_map);
+      for (i = 0; i < d->n_vars; i++)
+        {
+          var_set_vardict (d->vars[i].var, &d->vars[i]);
+          hmap_insert_fast (&d->name_map, &d->vars[i].name_node,
+                            d->vars[i].name_node.hash);
+        }
     }
     }
-  d->var[d->var_cnt++] = v;
-  hsh_force_insert (d->name_tab, v);
 
 
-  if ( d->changed ) d->changed (d, d->changed_data);
-  if ( d->callbacks &&  d->callbacks->var_added )
+  vardict = &d->vars[d->n_vars++];
+  vardict->dict = d;
+  vardict->var = v;
+  hmap_insert (&d->name_map, &vardict->name_node,
+               utf8_hash_case_string (var_get_name (v), 0));
+  vardict->case_index = case_index;
+  var_set_vardict (v, vardict);
+
+  if (d->changed) d->changed (d, d->changed_data);
+  if (d->callbacks &&  d->callbacks->var_added)
     d->callbacks->var_added (d, var_get_dict_index (v), d->cb_data);
 
     d->callbacks->var_added (d, var_get_dict_index (v), d->cb_data);
 
-  d->next_value_idx++;
   invalidate_proto (d);
   invalidate_proto (d);
+  d->next_value_idx = case_index + 1;
 
   return v;
 }
 
 
   return v;
 }
 
+static struct variable *
+add_var (struct dictionary *d, struct variable *v)
+{
+  return add_var_with_case_index (d, v, d->next_value_idx);
+}
+
 /* Creates and returns a new variable in D with the given NAME
    and WIDTH.  Returns a null pointer if the given NAME would
    duplicate that of an existing variable in the dictionary. */
 /* Creates and returns a new variable in D with the given NAME
    and WIDTH.  Returns a null pointer if the given NAME would
    duplicate that of an existing variable in the dictionary. */
@@ -410,27 +820,46 @@ dict_create_var_assert (struct dictionary *d, const char *name, int width)
   return add_var (d, var_create (name, width));
 }
 
   return add_var (d, var_create (name, width));
 }
 
-/* Creates and returns a new variable in D with name NAME, as a
-   copy of existing variable OLD_VAR, which need not be in D or
-   in any dictionary.  Returns a null pointer if the given NAME
-   would duplicate that of an existing variable in the
+/* Creates and returns a new variable in D, as a copy of existing variable
+   OLD_VAR, which need not be in D or in any dictionary.  Returns a null
+   pointer if OLD_VAR's name would duplicate that of an existing variable in
+   the dictionary. */
+struct variable *
+dict_clone_var (struct dictionary *d, const struct variable *old_var)
+{
+  return dict_clone_var_as (d, old_var, var_get_name (old_var));
+}
+
+/* Creates and returns a new variable in D, as a copy of existing variable
+   OLD_VAR, which need not be in D or in any dictionary.  Assert-fails if
+   OLD_VAR's name would duplicate that of an existing variable in the
    dictionary. */
 struct variable *
    dictionary. */
 struct variable *
-dict_clone_var (struct dictionary *d, const struct variable *old_var,
-                const char *name)
+dict_clone_var_assert (struct dictionary *d, const struct variable *old_var)
+{
+  return dict_clone_var_as_assert (d, old_var, var_get_name (old_var));
+}
+
+/* Creates and returns a new variable in D with name NAME, as a copy of
+   existing variable OLD_VAR, which need not be in D or in any dictionary.
+   Returns a null pointer if the given NAME would duplicate that of an existing
+   variable in the dictionary. */
+struct variable *
+dict_clone_var_as (struct dictionary *d, const struct variable *old_var,
+                   const char *name)
 {
   return (dict_lookup_var (d, name) == NULL
 {
   return (dict_lookup_var (d, name) == NULL
-          ? dict_clone_var_assert (d, old_var, name)
+          ? dict_clone_var_as_assert (d, old_var, name)
           : NULL);
 }
 
           : NULL);
 }
 
-/* Creates and returns a new variable in D with name NAME, as a
-   copy of existing variable OLD_VAR, which need not be in D or
-   in any dictionary.  Assert-fails if the given NAME would
-   duplicate that of an existing variable in the dictionary. */
+/* Creates and returns a new variable in D with name NAME, as a copy of
+   existing variable OLD_VAR, which need not be in D or in any dictionary.
+   Assert-fails if the given NAME would duplicate that of an existing variable
+   in the dictionary. */
 struct variable *
 struct variable *
-dict_clone_var_assert (struct dictionary *d, const struct variable *old_var,
-                       const char *name)
+dict_clone_var_as_assert (struct dictionary *d, const struct variable *old_var,
+                          const char *name)
 {
   struct variable *new_var = var_clone (old_var);
   assert (dict_lookup_var (d, name) == NULL);
 {
   struct variable *new_var = var_clone (old_var);
   assert (dict_lookup_var (d, name) == NULL);
@@ -438,202 +867,50 @@ dict_clone_var_assert (struct dictionary *d, const struct variable *old_var,
   return add_var (d, new_var);
 }
 
   return add_var (d, new_var);
 }
 
-/* Returns the variable named NAME in D, or a null pointer if no
-   variable has that name. */
 struct variable *
 struct variable *
-dict_lookup_var (const struct dictionary *d, const char *name)
+dict_clone_var_in_place_assert (struct dictionary *d,
+                                const struct variable *old_var)
 {
 {
-  struct variable *target ;
-  struct variable *result ;
-
-  if ( ! var_is_plausible_name (name, false))
-    return NULL;
-
-  target = var_create (name, 0);
-  result = hsh_find (d->name_tab, target);
-  var_destroy (target);
-
-  if ( result && var_has_vardict (result)) 
-  {
-      const struct vardict_info *vdi = var_get_vardict (result);
-      assert (vdi->dict == d);
-  }
-
-  return result;
+  assert (dict_lookup_var (d, var_get_name (old_var)) == NULL);
+  return add_var_with_case_index (d, var_clone (old_var),
+                                  var_get_case_index (old_var));
 }
 
 }
 
-/* Returns the variable named NAME in D.  Assert-fails if no
+/* Returns the variable named NAME in D, or a null pointer if no
    variable has that name. */
 struct variable *
    variable has that name. */
 struct variable *
-dict_lookup_var_assert (const struct dictionary *d, const char *name)
-{
-  struct variable *v = dict_lookup_var (d, name);
-  assert (v != NULL);
-  return v;
-}
-
-/* Returns true if variable V is in dictionary D,
-   false otherwise. */
-bool
-dict_contains_var (const struct dictionary *d, const struct variable *v)
-{
-  if (var_has_vardict (v))
-    {
-      const struct vardict_info *vdi = var_get_vardict (v);
-      return (vdi->dict_index >= 0
-              && vdi->dict_index < d->var_cnt
-              && d->var[vdi->dict_index] == v);
-    }
-  else
-    return false;
-}
-
-/* Compares two double pointers to variables, which should point
-   to elements of a struct dictionary's `var' member array. */
-static int
-compare_var_ptrs (const void *a_, const void *b_, const void *aux UNUSED)
-{
-  struct variable *const *a = a_;
-  struct variable *const *b = b_;
-
-  return *a < *b ? -1 : *a > *b;
-}
-
-/* Sets the dict_index in V's vardict to DICT_INDEX. */
-static void
-set_var_dict_index (struct variable *v, int dict_index)
-{
-  struct vardict_info vdi = *var_get_vardict (v);
-  struct dictionary *d = vdi.dict;
-  vdi.dict_index = dict_index;
-  var_set_vardict (v, &vdi);
-
-  if ( d->changed ) d->changed (d, d->changed_data);
-  if ( d->callbacks &&  d->callbacks->var_changed )
-    d->callbacks->var_changed (d, dict_index, d->cb_data);
-}
-
-/* Sets the case_index in V's vardict to CASE_INDEX. */
-static void
-set_var_case_index (struct variable *v, int case_index)
-{
-  struct vardict_info vdi = *var_get_vardict (v);
-  vdi.case_index = case_index;
-  var_set_vardict (v, &vdi);
-}
-
-/* Re-sets the dict_index in the dictionary variables with
-   indexes from FROM to TO (exclusive). */
-static void
-reindex_vars (struct dictionary *d, size_t from, size_t to)
-{
-  size_t i;
-
-  for (i = from; i < to; i++)
-    set_var_dict_index (d->var[i], i);
-}
-
-/* Deletes variable V from dictionary D and frees V.
-
-   This is a very bad idea if there might be any pointers to V
-   from outside D.  In general, no variable in the active file's
-   dictionary should be deleted when any transformations are
-   active on the dictionary's dataset, because those
-   transformations might reference the deleted variable.  The
-   safest time to delete a variable is just after a procedure has
-   been executed, as done by DELETE VARIABLES.
-
-   Pointers to V within D are not a problem, because
-   dict_delete_var() knows to remove V from split variables,
-   weights, filters, etc. */
-void
-dict_delete_var (struct dictionary *d, struct variable *v)
-{
-  int dict_index = var_get_dict_index (v);
-  const int case_index = var_get_case_index (v);
-  const int width = var_get_width (v);
-
-  assert (dict_contains_var (d, v));
-
-  /* Delete aux data. */
-  var_clear_aux (v);
-
-  dict_unset_split_var (d, v);
-
-  if (d->weight == v)
-    dict_set_weight (d, NULL);
-
-  if (d->filter == v)
-    dict_set_filter (d, NULL);
-
-  dict_clear_vectors (d);
-
-  /* Remove V from var array. */
-  remove_element (d->var, d->var_cnt, sizeof *d->var, dict_index);
-  d->var_cnt--;
-
-  /* Update dict_index for each affected variable. */
-  reindex_vars (d, dict_index, d->var_cnt);
-
-  /* Update name hash. */
-  hsh_force_delete (d->name_tab, v);
-
-
-  /* Free memory. */
-  var_clear_vardict (v);
-  var_destroy (v);
-
-  if ( d->changed ) d->changed (d, d->changed_data);
-
-  invalidate_proto (d);
-  if (d->callbacks &&  d->callbacks->var_deleted )
-    d->callbacks->var_deleted (d, dict_index, case_index, width, d->cb_data);
-}
-
-/* Deletes the COUNT variables listed in VARS from D.  This is
-   unsafe; see the comment on dict_delete_var() for details. */
-void
-dict_delete_vars (struct dictionary *d,
-                  struct variable *const *vars, size_t count)
-{
-  /* FIXME: this can be done in O(count) time, but this algorithm
-     is O(count**2). */
-  assert (d != NULL);
-  assert (count == 0 || vars != NULL);
-
-  while (count-- > 0)
-    dict_delete_var (d, *vars++);
-}
-
-/* Deletes the COUNT variables in D starting at index IDX.  This
-   is unsafe; see the comment on dict_delete_var() for
-   details. */
-void
-dict_delete_consecutive_vars (struct dictionary *d, size_t idx, size_t count)
+dict_lookup_var (const struct dictionary *d, const char *name)
 {
 {
-  /* FIXME: this can be done in O(count) time, but this algorithm
-     is O(count**2). */
-  assert (idx + count <= d->var_cnt);
+  struct vardict_info *vardict;
 
 
-  while (count-- > 0)
-    dict_delete_var (d, d->var[idx]);
+  HMAP_FOR_EACH_WITH_HASH (vardict, struct vardict_info, name_node,
+                           utf8_hash_case_string (name, 0), &d->name_map)
+    {
+      struct variable *var = vardict->var;
+      if (!utf8_strcasecmp (var_get_name (var), name))
+        return var;
+    }
+
+  return NULL;
 }
 
 }
 
-/* Deletes scratch variables from dictionary D. */
-void
-dict_delete_scratch_vars (struct dictionary *d)
+/* Returns the variable named NAME in D.  Assert-fails if no
+   variable has that name. */
+struct variable *
+dict_lookup_var_assert (const struct dictionary *d, const char *name)
 {
 {
-  int i;
-
-  /* FIXME: this can be done in O(count) time, but this algorithm
-     is O(count**2). */
-  assert (d != NULL);
+  struct variable *v = dict_lookup_var (d, name);
+  assert (v != NULL);
+  return v;
+}
 
 
-  for (i = 0; i < d->var_cnt; )
-    if (var_get_dict_class (d->var[i]) == DC_SCRATCH)
-      dict_delete_var (d, d->var[i]);
-    else
-      i++;
+/* Returns true if variable V is in dictionary D,
+   false otherwise. */
+bool
+dict_contains_var (const struct dictionary *d, const struct variable *v)
+{
+  return (var_has_vardict (v)
+          && vardict_get_dictionary (var_get_vardict (v)) == d);
 }
 
 /* Moves V to 0-based position IDX in D.  Other variables in D,
 }
 
 /* Moves V to 0-based position IDX in D.  Other variables in D,
@@ -644,9 +921,11 @@ dict_reorder_var (struct dictionary *d, struct variable *v, size_t new_index)
 {
   size_t old_index = var_get_dict_index (v);
 
 {
   size_t old_index = var_get_dict_index (v);
 
-  assert (new_index < d->var_cnt);
-  move_element (d->var, d->var_cnt, sizeof *d->var, old_index, new_index);
-  reindex_vars (d, MIN (old_index, new_index), MAX (old_index, new_index) + 1);
+  assert (new_index < d->n_vars);
+
+  unindex_vars (d, MIN (old_index, new_index), MAX (old_index, new_index) + 1);
+  move_element (d->vars, d->n_vars, sizeof *d->vars, old_index, new_index);
+  reindex_vars (d, MIN (old_index, new_index), MAX (old_index, new_index) + 1, false);
 }
 
 /* Reorders the variables in D, placing the COUNT variables
 }
 
 /* Reorders the variables in D, placing the COUNT variables
@@ -657,46 +936,78 @@ void
 dict_reorder_vars (struct dictionary *d,
                    struct variable *const *order, size_t count)
 {
 dict_reorder_vars (struct dictionary *d,
                    struct variable *const *order, size_t count)
 {
-  struct variable **new_var;
+  struct vardict_info *new_var;
   size_t i;
 
   size_t i;
 
-  assert (d != NULL);
   assert (count == 0 || order != NULL);
   assert (count == 0 || order != NULL);
-  assert (count <= d->var_cnt);
+  assert (count <= d->n_vars);
+
+  new_var = xnmalloc (d->allocated_vars, sizeof *new_var);
 
 
-  new_var = xnmalloc (d->var_cnt, sizeof *new_var);
-  memcpy (new_var, order, count * sizeof *new_var);
+  /* Add variables in ORDER to new_var. */
   for (i = 0; i < count; i++)
     {
   for (i = 0; i < count; i++)
     {
-      size_t index = var_get_dict_index (order[i]);
-      assert (d->var[index] == order[i]);
-      d->var[index] = NULL;
-      set_var_dict_index (order[i], i);
+      struct vardict_info *old_var;
+
+      assert (dict_contains_var (d, order[i]));
+
+      old_var = var_get_vardict (order[i]);
+      new_var[i] = *old_var;
+      old_var->dict = NULL;
     }
     }
-  for (i = 0; i < d->var_cnt; i++)
-    if (d->var[i] != NULL)
-      {
-        assert (count < d->var_cnt);
-        new_var[count] = d->var[i];
-        set_var_dict_index (new_var[count], count);
-        count++;
-      }
-  free (d->var);
-  d->var = new_var;
+
+  /* Add remaining variables to new_var. */
+  for (i = 0; i < d->n_vars; i++)
+    if (d->vars[i].dict != NULL)
+      new_var[count++] = d->vars[i];
+  assert (count == d->n_vars);
+
+  /* Replace old vardicts by new ones. */
+  free (d->vars);
+  d->vars = new_var;
+
+  hmap_clear (&d->name_map);
+  reindex_vars (d, 0, d->n_vars, false);
 }
 
 }
 
-/* Changes the name of variable V in dictionary D to NEW_NAME. */
+/* Changes the name of variable V that is currently in a dictionary to
+   NEW_NAME. */
 static void
 static void
-rename_var (struct dictionary *d, struct variable *v, const char *new_name)
+rename_var (struct variable *v, const char *new_name)
 {
 {
-  struct vardict_info vdi;
-
-  assert (dict_contains_var (d, v));
-
-  vdi = *var_get_vardict (v);
+  struct vardict_info *vardict = var_get_vardict (v);
   var_clear_vardict (v);
   var_set_name (v, new_name);
   var_clear_vardict (v);
   var_set_name (v, new_name);
-  var_set_vardict (v, &vdi);
+  vardict->name_node.hash = utf8_hash_case_string (new_name, 0);
+  var_set_vardict (v, vardict);
+}
+
+/* Tries to changes the name of V in D to name NEW_NAME.  Returns true if
+   successful, false if a variable (other than V) with the given name already
+   exists in D. */
+bool
+dict_try_rename_var (struct dictionary *d, struct variable *v,
+                     const char *new_name)
+{
+  struct variable *conflict = dict_lookup_var (d, new_name);
+  if (conflict && v != conflict)
+    return false;
+
+  struct variable *old = var_clone (v);
+  unindex_var (d, var_get_vardict (v));
+  rename_var (v, new_name);
+  reindex_var (d, var_get_vardict (v), false);
+
+  if (settings_get_algorithm () == ENHANCED)
+    var_clear_short_names (v);
+
+  if (d->changed) d->changed (d, d->changed_data);
+  if (d->callbacks &&  d->callbacks->var_changed)
+    d->callbacks->var_changed (d, var_get_dict_index (v), VAR_TRAIT_NAME, old, d->cb_data);
+
+  var_unref (old);
+
+  return true;
 }
 
 /* Changes the name of V in D to name NEW_NAME.  Assert-fails if
 }
 
 /* Changes the name of V in D to name NEW_NAME.  Assert-fails if
@@ -706,19 +1017,8 @@ void
 dict_rename_var (struct dictionary *d, struct variable *v,
                  const char *new_name)
 {
 dict_rename_var (struct dictionary *d, struct variable *v,
                  const char *new_name)
 {
-  assert (!strcasecmp (var_get_name (v), new_name)
-          || dict_lookup_var (d, new_name) == NULL);
-
-  hsh_force_delete (d->name_tab, v);
-  rename_var (d, v, new_name);
-  hsh_force_insert (d->name_tab, v);
-
-  if (settings_get_algorithm () == ENHANCED)
-    var_clear_short_names (v);
-
-  if ( d->changed ) d->changed (d, d->changed_data);
-  if ( d->callbacks &&  d->callbacks->var_changed )
-    d->callbacks->var_changed (d, var_get_dict_index (v), d->cb_data);
+  bool ok UNUSED = dict_try_rename_var (d, v, new_name);
+  assert (ok);
 }
 
 /* Renames COUNT variables specified in VARS to the names given
 }
 
 /* Renames COUNT variables specified in VARS to the names given
@@ -749,34 +1049,37 @@ dict_rename_vars (struct dictionary *d,
      and rename them. */
   for (i = 0; i < count; i++)
     {
      and rename them. */
   for (i = 0; i < count; i++)
     {
-      hsh_force_delete (d->name_tab, vars[i]);
-      rename_var (d, vars[i], new_names[i]);
+      unindex_var (d, var_get_vardict (vars[i]));
+      rename_var (vars[i], new_names[i]);
     }
 
   /* Add the renamed variables back into the name hash,
      checking for conflicts. */
   for (i = 0; i < count; i++)
     }
 
   /* Add the renamed variables back into the name hash,
      checking for conflicts. */
   for (i = 0; i < count; i++)
-    if (hsh_insert (d->name_tab, vars[i]) != NULL)
-      {
-        /* There is a name conflict.
-           Back out all the name changes that have already
-           taken place, and indicate failure. */
-        size_t fail_idx = i;
-        if (err_name != NULL)
-          *err_name = new_names[i];
-
-        for (i = 0; i < fail_idx; i++)
-          hsh_force_delete (d->name_tab, vars[i]);
-
-        for (i = 0; i < count; i++)
-          {
-            rename_var (d, vars[i], old_names[i]);
-            hsh_force_insert (d->name_tab, vars[i]);
-          }
-
-        pool_destroy (pool);
-        return false;
-      }
+    {
+      if (dict_lookup_var (d, var_get_name (vars[i])) != NULL)
+        {
+          /* There is a name conflict.
+             Back out all the name changes that have already
+             taken place, and indicate failure. */
+          size_t fail_idx = i;
+          if (err_name != NULL)
+            *err_name = new_names[i];
+
+          for (i = 0; i < fail_idx; i++)
+            unindex_var (d, var_get_vardict (vars[i]));
+
+          for (i = 0; i < count; i++)
+            {
+              rename_var (vars[i], old_names[i]);
+              reindex_var (d, var_get_vardict (vars[i]), false);
+            }
+
+          pool_destroy (pool);
+          return false;
+        }
+      reindex_var (d, var_get_vardict (vars[i]), false);
+    }
 
   /* Clear short names. */
   if (settings_get_algorithm () == ENHANCED)
 
   /* Clear short names. */
   if (settings_get_algorithm () == ENHANCED)
@@ -799,63 +1102,78 @@ var_name_is_insertable (const struct dictionary *dict, const char *name)
           && lex_id_to_token (ss_cstr (name)) == T_ID);
 }
 
           && lex_id_to_token (ss_cstr (name)) == T_ID);
 }
 
-static bool
-make_hinted_name (const struct dictionary *dict, const char *hint,
-                  char name[VAR_NAME_LEN + 1])
+static char *
+make_hinted_name (const struct dictionary *dict, const char *hint)
 {
 {
+  size_t hint_len = strlen (hint);
   bool dropped = false;
   bool dropped = false;
-  char *cp;
-
-  for (cp = name; *hint && cp < name + VAR_NAME_LEN; hint++)
+  char *root, *rp;
+  size_t ofs;
+  int mblen;
+
+  if (hint_len > ID_MAX_LEN)
+    hint_len = ID_MAX_LEN;
+
+  /* The allocation size here is OK: characters that are copied directly fit
+     OK, and characters that are not copied directly are replaced by a single
+     '_' byte.  If u8_mbtouc() replaces bad input by 0xfffd, then that will get
+     replaced by '_' too.  */
+  root = rp = xmalloc (hint_len + 1);
+  for (ofs = 0; ofs < hint_len; ofs += mblen)
     {
     {
-      if (cp == name
-          ? lex_is_id1 (*hint) && *hint != '$'
-          : lex_is_idn (*hint))
+      ucs4_t uc;
+
+      mblen = u8_mbtouc (&uc, CHAR_CAST (const uint8_t *, hint + ofs),
+                         hint_len - ofs);
+      if (rp == root
+          ? lex_uc_is_id1 (uc) && uc != '$'
+          : lex_uc_is_idn (uc))
         {
           if (dropped)
             {
         {
           if (dropped)
             {
-              *cp++ = '_';
+              *rp++ = '_';
               dropped = false;
             }
               dropped = false;
             }
-          if (cp < name + VAR_NAME_LEN)
-            *cp++ = *hint;
+          rp += u8_uctomb (CHAR_CAST (uint8_t *, rp), uc, 6);
         }
         }
-      else if (cp > name)
+      else if (rp != root)
         dropped = true;
     }
         dropped = true;
     }
-  *cp = '\0';
+  *rp = '\0';
 
 
-  if (name[0] != '\0')
+  if (root[0] != '\0')
     {
     {
-      size_t len = strlen (name);
       unsigned long int i;
 
       unsigned long int i;
 
-      if (var_name_is_insertable (dict, name))
-        return true;
+      if (var_name_is_insertable (dict, root))
+        return root;
 
       for (i = 0; i < ULONG_MAX; i++)
         {
           char suffix[INT_BUFSIZE_BOUND (i) + 1];
 
       for (i = 0; i < ULONG_MAX; i++)
         {
           char suffix[INT_BUFSIZE_BOUND (i) + 1];
-          int ofs;
+          char *name;
 
           suffix[0] = '_';
 
           suffix[0] = '_';
-          if (!str_format_26adic (i + 1, &suffix[1], sizeof suffix - 1))
+          if (!str_format_26adic (i + 1, true, &suffix[1], sizeof suffix - 1))
             NOT_REACHED ();
 
             NOT_REACHED ();
 
-          ofs = MIN (VAR_NAME_LEN - strlen (suffix), len);
-          strcpy (&name[ofs], suffix);
-
+          name = utf8_encoding_concat (root, suffix, dict->encoding, 64);
           if (var_name_is_insertable (dict, name))
           if (var_name_is_insertable (dict, name))
-            return true;
+            {
+              free (root);
+              return name;
+            }
+          free (name);
         }
     }
 
         }
     }
 
-  return false;
+  free (root);
+
+  return NULL;
 }
 
 }
 
-static bool
-make_numeric_name (const struct dictionary *dict, unsigned long int *num_start,
-                   char name[VAR_NAME_LEN + 1])
+static char *
+make_numeric_name (const struct dictionary *dict, unsigned long int *num_start)
 {
   unsigned long int number;
 
 {
   unsigned long int number;
 
@@ -863,27 +1181,24 @@ make_numeric_name (const struct dictionary *dict, unsigned long int *num_start,
        number < ULONG_MAX;
        number++)
     {
        number < ULONG_MAX;
        number++)
     {
+      char name[3 + INT_STRLEN_BOUND (number) + 1];
+
       sprintf (name, "VAR%03lu", number);
       if (dict_lookup_var (dict, name) == NULL)
         {
           if (num_start != NULL)
             *num_start = number + 1;
       sprintf (name, "VAR%03lu", number);
       if (dict_lookup_var (dict, name) == NULL)
         {
           if (num_start != NULL)
             *num_start = number + 1;
-          return true;
+          return xstrdup (name);
         }
     }
 
         }
     }
 
-  if (num_start != NULL)
-    *num_start = ULONG_MAX;
-  return false;
+  NOT_REACHED ();
 }
 
 
 }
 
 
-/* Attempts to devise a variable name unique within DICT.
-   Returns true if successful, in which case the new variable
-   name is stored into NAME.  Returns false if all names that can
-   be generated have already been taken.  (Returning false is
-   quite unlikely: at least ULONG_MAX unique names can be
-   generated.)
+/* Devises and returns a variable name unique within DICT.  The variable name
+   is owned by the caller, which must free it with free() when it is no longer
+   needed.
 
    HINT, if it is non-null, is used as a suggestion that will be
    modified for suitability as a variable name and for
 
    HINT, if it is non-null, is used as a suggestion that will be
    modified for suitability as a variable name and for
@@ -894,14 +1209,40 @@ make_numeric_name (const struct dictionary *dict, unsigned long int *num_start,
    value is used.  If NUM_START is non-null, then its value is
    used as the minimum numeric value to check, and it is updated
    to the next value to be checked.
    value is used.  If NUM_START is non-null, then its value is
    used as the minimum numeric value to check, and it is updated
    to the next value to be checked.
-   */
-bool
+*/
+char *
 dict_make_unique_var_name (const struct dictionary *dict, const char *hint,
 dict_make_unique_var_name (const struct dictionary *dict, const char *hint,
-                           unsigned long int *num_start,
-                           char name[VAR_NAME_LEN + 1])
+                           unsigned long int *num_start)
+{
+  if (hint != NULL)
+    {
+      char *hinted_name = make_hinted_name (dict, hint);
+      if (hinted_name != NULL)
+        return hinted_name;
+    }
+
+  return make_numeric_name (dict, num_start);
+}
+
+/* Returns whether variable names must be valid identifiers.  Normally, this is
+   true, but sometimes a dictionary is prepared for external use (e.g. output
+   to a CSV file) where names don't have to be valid. */
+bool
+dict_get_names_must_be_ids (const struct dictionary *d)
 {
 {
-  return ((hint != NULL && make_hinted_name (dict, hint, name))
-          || make_numeric_name (dict, num_start, name));
+  return d->names_must_be_ids;
+}
+
+/* Sets whether variable names must be valid identifiers.  Normally, this is
+   true, but sometimes a dictionary is prepared for external use (e.g. output
+   to a CSV file) where names don't have to be valid.
+
+   Changing this setting from false to true doesn't make the dictionary check
+   all the existing variable names, so it can cause an invariant violation. */
+void
+dict_set_names_must_be_ids (struct dictionary *d, bool names_must_be_ids)
+{
+  d->names_must_be_ids = names_must_be_ids;
 }
 
 /* Returns the weighting variable in dictionary D, or a null
 }
 
 /* Returns the weighting variable in dictionary D, or a null
@@ -909,7 +1250,6 @@ dict_make_unique_var_name (const struct dictionary *dict, const char *hint,
 struct variable *
 dict_get_weight (const struct dictionary *d)
 {
 struct variable *
 dict_get_weight (const struct dictionary *d)
 {
-  assert (d != NULL);
   assert (d->weight == NULL || dict_contains_var (d, d->weight));
 
   return d->weight;
   assert (d->weight == NULL || dict_contains_var (d, d->weight));
 
   return d->weight;
@@ -925,7 +1265,6 @@ double
 dict_get_case_weight (const struct dictionary *d, const struct ccase *c,
                      bool *warn_on_invalid)
 {
 dict_get_case_weight (const struct dictionary *d, const struct ccase *c,
                      bool *warn_on_invalid)
 {
-  assert (d != NULL);
   assert (c != NULL);
 
   if (d->weight == NULL)
   assert (c != NULL);
 
   if (d->weight == NULL)
@@ -933,34 +1272,33 @@ dict_get_case_weight (const struct dictionary *d, const struct ccase *c,
   else
     {
       double w = case_num (c, d->weight);
   else
     {
       double w = case_num (c, d->weight);
-      if (w < 0.0 || var_is_num_missing (d->weight, w, MV_ANY))
-        w = 0.0;
-      if ( w == 0.0 && warn_on_invalid != NULL && *warn_on_invalid ) {
-         *warn_on_invalid = false;
-         msg (SW, _("At least one case in the data file had a weight value "
-                    "that was user-missing, system-missing, zero, or "
-                    "negative.  These case(s) were ignored."));
-      }
-      return w;
+
+      return var_force_valid_weight (d->weight, w, warn_on_invalid);
     }
 }
 
     }
 }
 
+/* Returns the format to use for weights. */
+const struct fmt_spec *
+dict_get_weight_format (const struct dictionary *d)
+{
+  return d->weight ? var_get_print_format (d->weight) : &F_8_0;
+}
+
 /* Sets the weighting variable of D to V, or turning off
    weighting if V is a null pointer. */
 void
 dict_set_weight (struct dictionary *d, struct variable *v)
 {
 /* Sets the weighting variable of D to V, or turning off
    weighting if V is a null pointer. */
 void
 dict_set_weight (struct dictionary *d, struct variable *v)
 {
-  assert (d != NULL);
   assert (v == NULL || dict_contains_var (d, v));
   assert (v == NULL || var_is_numeric (v));
 
   d->weight = v;
 
   if (d->changed) d->changed (d, d->changed_data);
   assert (v == NULL || dict_contains_var (d, v));
   assert (v == NULL || var_is_numeric (v));
 
   d->weight = v;
 
   if (d->changed) d->changed (d, d->changed_data);
-  if ( d->callbacks &&  d->callbacks->weight_changed )
+  if (d->callbacks &&  d->callbacks->weight_changed)
     d->callbacks->weight_changed (d,
     d->callbacks->weight_changed (d,
-                                 v ? var_get_dict_index (v) : -1,
-                                 d->cb_data);
+                                  v ? var_get_dict_index (v) : -1,
+                                  d->cb_data);
 }
 
 /* Returns the filter variable in dictionary D (see cmd_filter())
 }
 
 /* Returns the filter variable in dictionary D (see cmd_filter())
@@ -968,7 +1306,6 @@ dict_set_weight (struct dictionary *d, struct variable *v)
 struct variable *
 dict_get_filter (const struct dictionary *d)
 {
 struct variable *
 dict_get_filter (const struct dictionary *d)
 {
-  assert (d != NULL);
   assert (d->filter == NULL || dict_contains_var (d, d->filter));
 
   return d->filter;
   assert (d->filter == NULL || dict_contains_var (d, d->filter));
 
   return d->filter;
@@ -979,17 +1316,16 @@ dict_get_filter (const struct dictionary *d)
 void
 dict_set_filter (struct dictionary *d, struct variable *v)
 {
 void
 dict_set_filter (struct dictionary *d, struct variable *v)
 {
-  assert (d != NULL);
   assert (v == NULL || dict_contains_var (d, v));
   assert (v == NULL || var_is_numeric (v));
 
   d->filter = v;
 
   if (d->changed) d->changed (d, d->changed_data);
   assert (v == NULL || dict_contains_var (d, v));
   assert (v == NULL || var_is_numeric (v));
 
   d->filter = v;
 
   if (d->changed) d->changed (d, d->changed_data);
-  if ( d->callbacks && d->callbacks->filter_changed )
+  if (d->callbacks && d->callbacks->filter_changed)
     d->callbacks->filter_changed (d,
     d->callbacks->filter_changed (d,
-                                 v ? var_get_dict_index (v) : -1,
-                                 d->cb_data);
+                                  v ? var_get_dict_index (v) : -1,
+                                      d->cb_data);
 }
 
 /* Returns the case limit for dictionary D, or zero if the number
 }
 
 /* Returns the case limit for dictionary D, or zero if the number
@@ -997,8 +1333,6 @@ dict_set_filter (struct dictionary *d, struct variable *v)
 casenumber
 dict_get_case_limit (const struct dictionary *d)
 {
 casenumber
 dict_get_case_limit (const struct dictionary *d)
 {
-  assert (d != NULL);
-
   return d->case_limit;
 }
 
   return d->case_limit;
 }
 
@@ -1007,8 +1341,6 @@ dict_get_case_limit (const struct dictionary *d)
 void
 dict_set_case_limit (struct dictionary *d, casenumber case_limit)
 {
 void
 dict_set_case_limit (struct dictionary *d, casenumber case_limit)
 {
-  assert (d != NULL);
-
   d->case_limit = case_limit;
 }
 
   d->case_limit = case_limit;
 }
 
@@ -1022,11 +1354,11 @@ dict_get_proto (const struct dictionary *d_)
       size_t i;
 
       d->proto = caseproto_create ();
       size_t i;
 
       d->proto = caseproto_create ();
-      d->proto = caseproto_reserve (d->proto, d->var_cnt);
-      for (i = 0; i < d->var_cnt; i++)
+      d->proto = caseproto_reserve (d->proto, d->n_vars);
+      for (i = 0; i < d->n_vars; i++)
         d->proto = caseproto_set_width (d->proto,
         d->proto = caseproto_set_width (d->proto,
-                                        var_get_case_index (d->var[i]),
-                                        var_get_width (d->var[i]));
+                                        var_get_case_index (d->vars[i].var),
+                                        var_get_width (d->vars[i].var));
     }
   return d->proto;
 }
     }
   return d->proto;
 }
@@ -1037,8 +1369,6 @@ dict_get_proto (const struct dictionary *d_)
 int
 dict_get_next_value_idx (const struct dictionary *d)
 {
 int
 dict_get_next_value_idx (const struct dictionary *d)
 {
-  assert (d != NULL);
-
   return d->next_value_idx;
 }
 
   return d->next_value_idx;
 }
 
@@ -1047,8 +1377,6 @@ dict_get_next_value_idx (const struct dictionary *d)
 size_t
 dict_get_case_size (const struct dictionary *d)
 {
 size_t
 dict_get_case_size (const struct dictionary *d)
 {
-  assert (d != NULL);
-
   return sizeof (union value) * dict_get_next_value_idx (d);
 }
 
   return sizeof (union value) * dict_get_next_value_idx (d);
 }
 
@@ -1060,9 +1388,9 @@ dict_compact_values (struct dictionary *d)
   size_t i;
 
   d->next_value_idx = 0;
   size_t i;
 
   d->next_value_idx = 0;
-  for (i = 0; i < d->var_cnt; i++)
+  for (i = 0; i < d->n_vars; i++)
     {
     {
-      struct variable *v = d->var[i];
+      struct variable *v = d->vars[i].var;
       set_var_case_index (v, d->next_value_idx++);
     }
   invalidate_proto (d);
       set_var_case_index (v, d->next_value_idx++);
     }
   invalidate_proto (d);
@@ -1081,21 +1409,18 @@ dict_compact_values (struct dictionary *d)
 size_t
 dict_count_values (const struct dictionary *d, unsigned int exclude_classes)
 {
 size_t
 dict_count_values (const struct dictionary *d, unsigned int exclude_classes)
 {
-  size_t i;
-  size_t cnt;
-
   assert ((exclude_classes & ~((1u << DC_ORDINARY)
                                | (1u << DC_SYSTEM)
                                | (1u << DC_SCRATCH))) == 0);
 
   assert ((exclude_classes & ~((1u << DC_ORDINARY)
                                | (1u << DC_SYSTEM)
                                | (1u << DC_SCRATCH))) == 0);
 
-  cnt = 0;
-  for (i = 0; i < d->var_cnt; i++)
+  size_t n = 0;
+  for (size_t i = 0; i < d->n_vars; i++)
     {
     {
-      enum dict_class class = var_get_dict_class (d->var[i]);
+      enum dict_class class = var_get_dict_class (d->vars[i].var);
       if (!(exclude_classes & (1u << class)))
       if (!(exclude_classes & (1u << class)))
-        cnt++;
+        n++;
     }
     }
-  return cnt;
+  return n;
 }
 
 /* Returns the case prototype that would result after deleting
 }
 
 /* Returns the case prototype that would result after deleting
@@ -1117,229 +1442,178 @@ dict_get_compacted_proto (const struct dictionary *d,
                                | (1u << DC_SCRATCH))) == 0);
 
   proto = caseproto_create ();
                                | (1u << DC_SCRATCH))) == 0);
 
   proto = caseproto_create ();
-  for (i = 0; i < d->var_cnt; i++)
+  for (i = 0; i < d->n_vars; i++)
     {
     {
-      struct variable *v = d->var[i];
+      struct variable *v = d->vars[i].var;
       if (!(exclude_classes & (1u << var_get_dict_class (v))))
         proto = caseproto_add_width (proto, var_get_width (v));
     }
   return proto;
 }
       if (!(exclude_classes & (1u << var_get_dict_class (v))))
         proto = caseproto_add_width (proto, var_get_width (v));
     }
   return proto;
 }
-\f
-/* Returns the SPLIT FILE vars (see cmd_split_file()).  Call
-   dict_get_split_cnt() to determine how many SPLIT FILE vars
-   there are.  Returns a null pointer if and only if there are no
-   SPLIT FILE vars. */
-const struct variable *const *
-dict_get_split_vars (const struct dictionary *d)
-{
-  assert (d != NULL);
-
-  return d->split;
-}
-
-/* Returns the number of SPLIT FILE vars. */
-size_t
-dict_get_split_cnt (const struct dictionary *d)
-{
-  assert (d != NULL);
-
-  return d->split_cnt;
-}
-
-/* Removes variable V, which must be in D, from D's set of split
-   variables. */
-void
-dict_unset_split_var (struct dictionary *d, struct variable *v)
-{
-  int orig_count;
-
-  assert (dict_contains_var (d, v));
-
-  orig_count = d->split_cnt;
-  d->split_cnt = remove_equal (d->split, d->split_cnt, sizeof *d->split,
-                               &v, compare_var_ptrs, NULL);
-  if (orig_count != d->split_cnt)
-    {
-      if (d->changed) d->changed (d, d->changed_data);
-      /* We changed the set of split variables so invoke the
-         callback. */
-      if (d->callbacks &&  d->callbacks->split_changed)
-        d->callbacks->split_changed (d, d->cb_data);
-    }
-}
-
-/* Sets CNT split vars SPLIT in dictionary D. */
-void
-dict_set_split_vars (struct dictionary *d,
-                     struct variable *const *split, size_t cnt)
-{
-  assert (d != NULL);
-  assert (cnt == 0 || split != NULL);
-
-  d->split_cnt = cnt;
-  if ( cnt > 0 )
-   {
-    d->split = xnrealloc (d->split, cnt, sizeof *d->split) ;
-    memcpy (d->split, split, cnt * sizeof *d->split);
-   }
-  else
-   {
-    free (d->split);
-    d->split = NULL;
-   }
-
-  if (d->changed) d->changed (d, d->changed_data);
-  if ( d->callbacks &&  d->callbacks->split_changed )
-    d->callbacks->split_changed (d, d->cb_data);
-}
-
 /* Returns the file label for D, or a null pointer if D is
    unlabeled (see cmd_file_label()). */
 const char *
 dict_get_label (const struct dictionary *d)
 {
 /* Returns the file label for D, or a null pointer if D is
    unlabeled (see cmd_file_label()). */
 const char *
 dict_get_label (const struct dictionary *d)
 {
-  assert (d != NULL);
-
   return d->label;
 }
 
   return d->label;
 }
 
-/* Sets D's file label to LABEL, truncating it to a maximum of 60
-   characters. */
+/* Sets D's file label to LABEL, truncating it to at most 60 bytes in D's
+   encoding.
+
+   Removes D's label if LABEL is null or the empty string. */
 void
 dict_set_label (struct dictionary *d, const char *label)
 {
 void
 dict_set_label (struct dictionary *d, const char *label)
 {
-  assert (d != NULL);
-
   free (d->label);
   free (d->label);
-  d->label = label != NULL ? xstrndup (label, 60) : NULL;
+  if (label == NULL || label[0] == '\0')
+    d->label = NULL;
+  else
+    d->label = utf8_encoding_trunc (label, d->encoding, 60);
 }
 
 }
 
-/* Returns the documents for D, or a null pointer if D has no
-   documents.  If the return value is nonnull, then the string
-   will be an exact multiple of DOC_LINE_LENGTH bytes in length,
-   with each segment corresponding to one line. */
-const char *
+/* Returns the documents for D, as an UTF-8 encoded string_array.  The
+   return value is always nonnull; if there are no documents then the
+   string_arary is empty.*/
+const struct string_array *
 dict_get_documents (const struct dictionary *d)
 {
 dict_get_documents (const struct dictionary *d)
 {
-  return ds_is_empty (&d->documents) ? NULL : ds_cstr (&d->documents);
+  return &d->documents;
 }
 
 }
 
-/* Sets the documents for D to DOCUMENTS, or removes D's
-   documents if DOCUMENT is a null pointer.  If DOCUMENTS is
-   nonnull, then it should be an exact multiple of
-   DOC_LINE_LENGTH bytes in length, with each segment
-   corresponding to one line. */
+/* Replaces the documents for D by NEW_DOCS, a UTF-8 encoded string_array. */
 void
 void
-dict_set_documents (struct dictionary *d, const char *documents)
+dict_set_documents (struct dictionary *d, const struct string_array *new_docs)
 {
 {
-  size_t remainder;
+  /* Swap out the old documents, instead of destroying them immediately, to
+     allow the new documents to include pointers into the old ones. */
+  struct string_array old_docs = STRING_ARRAY_INITIALIZER;
+  string_array_swap (&d->documents, &old_docs);
+
+  for (size_t i = 0; i < new_docs->n; i++)
+    dict_add_document_line (d, new_docs->strings[i], false);
+
+  string_array_destroy (&old_docs);
+}
 
 
-  ds_assign_cstr (&d->documents, documents != NULL ? documents : "");
+/* Replaces the documents for D by UTF-8 encoded string NEW_DOCS, dividing it
+   into individual lines at new-line characters.  Each line is truncated to at
+   most DOC_LINE_LENGTH bytes in D's encoding. */
+void
+dict_set_documents_string (struct dictionary *d, const char *new_docs)
+{
+  const char *s;
 
 
-  /* In case the caller didn't get it quite right, pad out the
-     final line with spaces. */
-  remainder = ds_length (&d->documents) % DOC_LINE_LENGTH;
-  if (remainder != 0)
-    ds_put_char_multiple (&d->documents, ' ', DOC_LINE_LENGTH - remainder);
+  dict_clear_documents (d);
+  for (s = new_docs; *s != '\0';)
+    {
+      size_t len = strcspn (s, "\n");
+      char *line = xmemdup0 (s, len);
+      dict_add_document_line (d, line, false);
+      free (line);
+
+      s += len;
+      if (*s == '\n')
+        s++;
+    }
 }
 
 /* Drops the documents from dictionary D. */
 void
 dict_clear_documents (struct dictionary *d)
 {
 }
 
 /* Drops the documents from dictionary D. */
 void
 dict_clear_documents (struct dictionary *d)
 {
-  ds_clear (&d->documents);
+  string_array_clear (&d->documents);
 }
 
 }
 
-/* Appends LINE to the documents in D.  LINE will be truncated or
-   padded on the right with spaces to make it exactly
-   DOC_LINE_LENGTH bytes long. */
-void
-dict_add_document_line (struct dictionary *d, const char *line)
+/* Appends the UTF-8 encoded LINE to the documents in D.  LINE will be
+   truncated so that it is no more than 80 bytes in the dictionary's
+   encoding.  If this causes some text to be lost, and ISSUE_WARNING is true,
+   then a warning will be issued. */
+bool
+dict_add_document_line (struct dictionary *d, const char *line,
+                        bool issue_warning)
 {
 {
-  if (strlen (line) > DOC_LINE_LENGTH)
+  size_t trunc_len;
+  bool truncated;
+
+  trunc_len = utf8_encoding_trunc_len (line, d->encoding, DOC_LINE_LENGTH);
+  truncated = line[trunc_len] != '\0';
+  if (truncated && issue_warning)
     {
     {
-      /* Note to translators: "bytes" is correct, not characters */
+      /* TRANSLATORS: "bytes" is correct, not characters due to UTF encoding */
       msg (SW, _("Truncating document line to %d bytes."), DOC_LINE_LENGTH);
     }
       msg (SW, _("Truncating document line to %d bytes."), DOC_LINE_LENGTH);
     }
-  buf_copy_str_rpad (ds_put_uninit (&d->documents, DOC_LINE_LENGTH),
-                     DOC_LINE_LENGTH, line, ' ');
+
+  string_array_append_nocopy (&d->documents, xmemdup0 (line, trunc_len));
+
+  return !truncated;
 }
 
 /* Returns the number of document lines in dictionary D. */
 size_t
 }
 
 /* Returns the number of document lines in dictionary D. */
 size_t
-dict_get_document_line_cnt (const struct dictionary *d)
+dict_get_document_n_lines (const struct dictionary *d)
 {
 {
-  return ds_length (&d->documents) / DOC_LINE_LENGTH;
+  return d->documents.n;
 }
 
 }
 
-/* Copies document line number IDX from dictionary D into
-   LINE, trimming off any trailing white space. */
-void
-dict_get_document_line (const struct dictionary *d,
-                        size_t idx, struct string *line)
+/* Returns document line number IDX in dictionary D.  The caller must not
+   modify or free the returned string. */
+const char *
+dict_get_document_line (const struct dictionary *d, size_t idx)
 {
 {
-  assert (idx < dict_get_document_line_cnt (d));
-  ds_assign_substring (line, ds_substr (&d->documents, idx * DOC_LINE_LENGTH,
-                                        DOC_LINE_LENGTH));
-  ds_rtrim (line, ss_cstr (CC_SPACES));
+  assert (idx < d->documents.n);
+  return d->documents.strings[idx];
 }
 
 }
 
-/* Creates in D a vector named NAME that contains the CNT
+/* Creates in D a vector named NAME that contains the N
    variables in VAR.  Returns true if successful, or false if a
    vector named NAME already exists in D. */
 bool
 dict_create_vector (struct dictionary *d,
                     const char *name,
    variables in VAR.  Returns true if successful, or false if a
    vector named NAME already exists in D. */
 bool
 dict_create_vector (struct dictionary *d,
                     const char *name,
-                    struct variable **var, size_t cnt)
+                    struct variable **var, size_t n)
 {
 {
-  size_t i;
-
-  assert (var != NULL);
-  assert (cnt > 0);
-  for (i = 0; i < cnt; i++)
+  assert (n > 0);
+  for (size_t i = 0; i < n; i++)
     assert (dict_contains_var (d, var[i]));
 
   if (dict_lookup_vector (d, name) == NULL)
     {
     assert (dict_contains_var (d, var[i]));
 
   if (dict_lookup_vector (d, name) == NULL)
     {
-      d->vector = xnrealloc (d->vector, d->vector_cnt + 1, sizeof *d->vector);
-      d->vector[d->vector_cnt++] = vector_create (name, var, cnt);
+      d->vector = xnrealloc (d->vector, d->n_vectors + 1, sizeof *d->vector);
+      d->vector[d->n_vectors++] = vector_create (name, var, n);
       return true;
     }
   else
     return false;
 }
 
       return true;
     }
   else
     return false;
 }
 
-/* Creates in D a vector named NAME that contains the CNT
+/* Creates in D a vector named NAME that contains the N
    variables in VAR.  A vector named NAME must not already exist
    in D. */
 void
 dict_create_vector_assert (struct dictionary *d,
                            const char *name,
    variables in VAR.  A vector named NAME must not already exist
    in D. */
 void
 dict_create_vector_assert (struct dictionary *d,
                            const char *name,
-                           struct variable **var, size_t cnt)
+                           struct variable **var, size_t n)
 {
   assert (dict_lookup_vector (d, name) == NULL);
 {
   assert (dict_lookup_vector (d, name) == NULL);
-  dict_create_vector (d, name, var, cnt);
+  dict_create_vector (d, name, var, n);
 }
 
 /* Returns the vector in D with index IDX, which must be less
 }
 
 /* Returns the vector in D with index IDX, which must be less
-   than dict_get_vector_cnt (D). */
+   than dict_get_n_vectors (D). */
 const struct vector *
 dict_get_vector (const struct dictionary *d, size_t idx)
 {
 const struct vector *
 dict_get_vector (const struct dictionary *d, size_t idx)
 {
-  assert (d != NULL);
-  assert (idx < d->vector_cnt);
+  assert (idx < d->n_vectors);
 
   return d->vector[idx];
 }
 
 /* Returns the number of vectors in D. */
 size_t
 
   return d->vector[idx];
 }
 
 /* Returns the number of vectors in D. */
 size_t
-dict_get_vector_cnt (const struct dictionary *d)
+dict_get_n_vectors (const struct dictionary *d)
 {
 {
-  assert (d != NULL);
-
-  return d->vector_cnt;
+  return d->n_vectors;
 }
 
 /* Looks up and returns the vector within D with the given
 }
 
 /* Looks up and returns the vector within D with the given
@@ -1348,8 +1622,8 @@ const struct vector *
 dict_lookup_vector (const struct dictionary *d, const char *name)
 {
   size_t i;
 dict_lookup_vector (const struct dictionary *d, const char *name)
 {
   size_t i;
-  for (i = 0; i < d->vector_cnt; i++)
-    if (!strcasecmp (vector_get_name (d->vector[i]), name))
+  for (i = 0; i < d->n_vectors; i++)
+    if (!utf8_strcasecmp (vector_get_name (d->vector[i]), name))
       return d->vector[i];
   return NULL;
 }
       return d->vector[i];
   return NULL;
 }
@@ -1360,20 +1634,151 @@ dict_clear_vectors (struct dictionary *d)
 {
   size_t i;
 
 {
   size_t i;
 
-  for (i = 0; i < d->vector_cnt; i++)
+  for (i = 0; i < d->n_vectors; i++)
     vector_destroy (d->vector[i]);
   free (d->vector);
 
   d->vector = NULL;
     vector_destroy (d->vector[i]);
   free (d->vector);
 
   d->vector = NULL;
-  d->vector_cnt = 0;
+  d->n_vectors = 0;
+}
+\f
+/* Multiple response sets. */
+
+/* Returns the multiple response set in DICT with index IDX, which must be
+   between 0 and the count returned by dict_get_n_mrsets(), exclusive. */
+const struct mrset *
+dict_get_mrset (const struct dictionary *dict, size_t idx)
+{
+  assert (idx < dict->n_mrsets);
+  return dict->mrsets[idx];
+}
+
+/* Returns the number of multiple response sets in DICT. */
+size_t
+dict_get_n_mrsets (const struct dictionary *dict)
+{
+  return dict->n_mrsets;
+}
+
+/* Looks for a multiple response set named NAME in DICT.  If it finds one,
+   returns its index; otherwise, returns SIZE_MAX. */
+static size_t
+dict_lookup_mrset_idx (const struct dictionary *dict, const char *name)
+{
+  size_t i;
+
+  for (i = 0; i < dict->n_mrsets; i++)
+    if (!utf8_strcasecmp (name, dict->mrsets[i]->name))
+      return i;
+
+  return SIZE_MAX;
+}
+
+/* Looks for a multiple response set named NAME in DICT.  If it finds one,
+   returns it; otherwise, returns NULL. */
+const struct mrset *
+dict_lookup_mrset (const struct dictionary *dict, const char *name)
+{
+  size_t idx = dict_lookup_mrset_idx (dict, name);
+  return idx != SIZE_MAX ? dict->mrsets[idx] : NULL;
 }
 
 }
 
+/* Adds MRSET to DICT, replacing any existing set with the same name.  Returns
+   true if a set was replaced, false if none existed with the specified name.
+
+   Ownership of MRSET is transferred to DICT. */
+bool
+dict_add_mrset (struct dictionary *dict, struct mrset *mrset)
+{
+  size_t idx;
+
+  assert (mrset_ok (mrset, dict));
+
+  idx = dict_lookup_mrset_idx (dict, mrset->name);
+  if (idx == SIZE_MAX)
+    {
+      dict->mrsets = xrealloc (dict->mrsets,
+                               (dict->n_mrsets + 1) * sizeof *dict->mrsets);
+      dict->mrsets[dict->n_mrsets++] = mrset;
+      return true;
+    }
+  else
+    {
+      mrset_destroy (dict->mrsets[idx]);
+      dict->mrsets[idx] = mrset;
+      return false;
+    }
+}
+
+/* Looks for a multiple response set in DICT named NAME.  If found, removes it
+   from DICT and returns true.  If none is found, returns false without
+   modifying DICT.
+
+   Deleting one multiple response set causes the indexes of other sets within
+   DICT to change. */
+bool
+dict_delete_mrset (struct dictionary *dict, const char *name)
+{
+  size_t idx = dict_lookup_mrset_idx (dict, name);
+  if (idx != SIZE_MAX)
+    {
+      mrset_destroy (dict->mrsets[idx]);
+      dict->mrsets[idx] = dict->mrsets[--dict->n_mrsets];
+      return true;
+    }
+  else
+    return false;
+}
+
+/* Deletes all multiple response sets from DICT. */
+void
+dict_clear_mrsets (struct dictionary *dict)
+{
+  size_t i;
+
+  for (i = 0; i < dict->n_mrsets; i++)
+    mrset_destroy (dict->mrsets[i]);
+  free (dict->mrsets);
+  dict->mrsets = NULL;
+  dict->n_mrsets = 0;
+}
+
+/* Removes VAR, which must be in DICT, from DICT's multiple response sets. */
+static void
+dict_unset_mrset_var (struct dictionary *dict, struct variable *var)
+{
+  size_t i;
+
+  assert (dict_contains_var (dict, var));
+
+  for (i = 0; i < dict->n_mrsets;)
+    {
+      struct mrset *mrset = dict->mrsets[i];
+      size_t j;
+
+      for (j = 0; j < mrset->n_vars;)
+        if (mrset->vars[j] == var)
+          remove_element (mrset->vars, mrset->n_vars--,
+                          sizeof *mrset->vars, j);
+        else
+          j++;
+
+      if (mrset->n_vars < 2)
+        {
+          mrset_destroy (mrset);
+          dict->mrsets[i] = dict->mrsets[--dict->n_mrsets];
+        }
+      else
+        i++;
+    }
+}
+\f
 /* Returns D's attribute set.  The caller may examine or modify
    the attribute set, but must not destroy it.  Destroying D or
    calling dict_set_attributes for D will also destroy D's
    attribute set. */
 struct attrset *
 /* Returns D's attribute set.  The caller may examine or modify
    the attribute set, but must not destroy it.  Destroying D or
    calling dict_set_attributes for D will also destroy D's
    attribute set. */
 struct attrset *
-dict_get_attributes (const struct dictionary *d) 
+dict_get_attributes (const struct dictionary *d)
 {
   return CONST_CAST (struct attrset *, &d->attributes);
 }
 {
   return CONST_CAST (struct attrset *, &d->attributes);
 }
@@ -1389,67 +1794,89 @@ dict_set_attributes (struct dictionary *d, const struct attrset *attrs)
 /* Returns true if D has at least one attribute in its attribute
    set, false if D's attribute set is empty. */
 bool
 /* Returns true if D has at least one attribute in its attribute
    set, false if D's attribute set is empty. */
 bool
-dict_has_attributes (const struct dictionary *d) 
+dict_has_attributes (const struct dictionary *d)
 {
   return attrset_count (&d->attributes) > 0;
 }
 
 {
   return attrset_count (&d->attributes) > 0;
 }
 
-/* Called from variable.c to notify the dictionary that some property of
-   the variable has changed */
+/* Called from variable.c to notify the dictionary that some property (indicated
+   by WHAT) of the variable has changed.  OLDVAR is a copy of V as it existed
+   prior to the change.  OLDVAR is destroyed by this function.
+*/
 void
 void
-dict_var_changed (const struct variable *v)
+dict_var_changed (const struct variable *v, unsigned int what, struct variable *oldvar)
 {
 {
-  if ( var_has_vardict (v))
+  if (var_has_vardict (v))
     {
     {
-      const struct vardict_info *vdi = var_get_vardict (v);
-      struct dictionary *d = vdi->dict;
+      const struct vardict_info *vardict = var_get_vardict (v);
+      struct dictionary *d = vardict->dict;
 
 
-      if ( NULL == d)
+      if (NULL == d)
        return;
 
        return;
 
-      if (d->changed ) d->changed (d, d->changed_data);
-      if ( d->callbacks && d->callbacks->var_changed )
-       d->callbacks->var_changed (d, var_get_dict_index (v), d->cb_data);
+      if (what & (VAR_TRAIT_WIDTH | VAR_TRAIT_POSITION))
+        invalidate_proto (d);
+
+      if (d->changed) d->changed (d, d->changed_data);
+      if (d->callbacks && d->callbacks->var_changed)
+        d->callbacks->var_changed (d, var_get_dict_index (v), what, oldvar, d->cb_data);
     }
     }
+  var_unref (oldvar);
 }
 
 
 }
 
 
-/* Called from variable.c to notify the dictionary that the variable's width
-   has changed */
-void
-dict_var_resized (const struct variable *v, int old_width)
+\f
+/* Dictionary used to contain "internal variables". */
+static struct dictionary *internal_dict;
+
+/* Create a variable of the specified WIDTH to be used for internal
+   calculations only.  The variable is assigned case index CASE_IDX. */
+struct variable *
+dict_create_internal_var (int case_idx, int width)
 {
 {
-  if ( var_has_vardict (v))
-    {
-      const struct vardict_info *vdi = var_get_vardict (v);
-      struct dictionary *d;
+  if (internal_dict == NULL)
+    internal_dict = dict_create ("UTF-8");
 
 
-      d = vdi->dict;
+  for (;;)
+    {
+      static int counter = INT_MAX / 2;
+      struct variable *var;
+      char name[64];
 
 
-      if (d->changed) d->changed (d, d->changed_data);
+      if (++counter == INT_MAX)
+        counter = INT_MAX / 2;
 
 
-      invalidate_proto (d);
-      if ( d->callbacks && d->callbacks->var_resized )
-       d->callbacks->var_resized (d, var_get_dict_index (v), old_width,
-                                   d->cb_data);
+      sprintf (name, "$internal%d", counter);
+      var = dict_create_var (internal_dict, name, width);
+      if (var != NULL)
+        {
+          set_var_case_index (var, case_idx);
+          return var;
+        }
     }
 }
 
     }
 }
 
-/* Called from variable.c to notify the dictionary that the variable's display width
-   has changed */
+/* Destroys VAR, which must have been created with
+   dict_create_internal_var(). */
 void
 void
-dict_var_display_width_changed (const struct variable *v)
+dict_destroy_internal_var (struct variable *var)
 {
 {
-  if ( var_has_vardict (v))
+  if (var != NULL)
     {
     {
-      const struct vardict_info *vdi = var_get_vardict (v);
-      struct dictionary *d;
-
-      d = vdi->dict;
+      dict_delete_var (internal_dict, var);
 
 
-      if (d->changed) d->changed (d, d->changed_data);
-      if ( d->callbacks && d->callbacks->var_display_width_changed )
-       d->callbacks->var_display_width_changed (d, var_get_dict_index (v), d->cb_data);
+      /* Destroy internal_dict if it has no variables left, just so that
+         valgrind --leak-check --show-reachable won't show internal_dict. */
+      if (dict_get_n_vars (internal_dict) == 0)
+        {
+          dict_unref (internal_dict);
+          internal_dict = NULL;
+        }
     }
 }
     }
 }
-
+\f
+int
+vardict_get_dict_index (const struct vardict_info *vardict)
+{
+  return vardict - vardict->dict->vars;
+}