RECODE: Improve error messages and coding style.
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 20 Sep 2022 06:13:16 +0000 (23:13 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Tue, 20 Sep 2022 06:13:16 +0000 (23:13 -0700)
src/language/xforms/recode.c
tests/language/stats/t-test.at
tests/language/xforms/recode.at

index 42fa9ff2157a531671b5aa2f173b56a1c203a10f..286b2ae6413541c2b6c3fe7b423cd5cfb95782e6 100644 (file)
@@ -70,6 +70,7 @@ struct map_out
     bool copy_input;            /* If true, copy input to output. */
     union value value;          /* If copy_input false, recoded value. */
     int width;                  /* If copy_input false, output value width. */
+    int ofs;                    /* Lexical location. */
   };
 
 /* Describes how to recode a single value or range of values into a
@@ -106,7 +107,10 @@ struct recode_trns
 static bool parse_src_vars (struct lexer *, struct recode_trns *, const struct dictionary *dict);
 static bool parse_mappings (struct lexer *, struct recode_trns *,
                             const char *dict_encoding);
-static bool parse_dst_vars (struct lexer *, struct recode_trns *, const struct dictionary *dict);
+static bool parse_dst_vars (struct lexer *, struct recode_trns *,
+                            const struct dictionary *,
+                            int src_start, int src_end,
+                            int mappings_start, int mappings_end);
 
 static void add_mapping (struct recode_trns *,
                          size_t *map_allocated, const struct map_in *);
@@ -114,18 +118,16 @@ static void add_mapping (struct recode_trns *,
 static bool parse_map_in (struct lexer *lexer, struct map_in *, struct pool *,
                           enum val_type src_type, size_t max_src_width,
                           const char *dict_encoding);
-static void set_map_in_generic (struct map_in *, enum map_in_type);
-static void set_map_in_num (struct map_in *, enum map_in_type, double, double);
 static void set_map_in_str (struct map_in *, struct pool *,
                             struct substring, size_t width,
                             const char *dict_encoding);
 
 static bool parse_map_out (struct lexer *lexer, struct pool *, struct map_out *);
-static void set_map_out_num (struct map_out *, double);
 static void set_map_out_str (struct map_out *, struct pool *,
                              struct substring);
 
-static bool enlarge_dst_widths (struct recode_trns *);
+static bool enlarge_dst_widths (struct lexer *, struct recode_trns *,
+                                int dst_start, int dst_end);
 static void create_dst_vars (struct recode_trns *, struct dictionary *);
 
 static bool recode_trns_free (void *trns_);
@@ -134,47 +136,71 @@ static const struct trns_class recode_trns_class;
 \f
 /* Parser. */
 
+static bool
+parse_one_recoding (struct lexer *lexer, struct dataset *ds,
+                    struct recode_trns *trns)
+{
+  struct dictionary *dict = dataset_dict (ds);
+
+  /* Parse source variable names,
+     then input to output mappings,
+     then destination variable names. */
+  int src_start = lex_ofs (lexer);
+  if (!parse_src_vars (lexer, trns, dict))
+    return false;
+  int src_end = lex_ofs (lexer) - 1;
+
+  int mappings_start = lex_ofs (lexer);
+  if (!parse_mappings (lexer, trns, dict_get_encoding (dict)))
+    return false;
+  int mappings_end = lex_ofs (lexer) - 1;
+
+  int dst_start = lex_ofs (lexer);
+  if (!parse_dst_vars (lexer, trns, dict,
+                       src_start, src_end, mappings_start, mappings_end))
+    return false;
+  int dst_end = lex_ofs (lexer) - 1;
+  if (dst_end < dst_start)
+    {
+      /* There was no target variable syntax, so the target variables are the
+         same as the source variables. */
+      dst_start = src_start;
+      dst_end = src_end;
+    }
+
+  /* Ensure that all the output strings are at least as wide
+     as the widest destination variable. */
+  if (trns->dst_type == VAL_STRING
+      && !enlarge_dst_widths (lexer, trns, dst_start, dst_end))
+    return false;
+
+  /* Create destination variables, if needed.
+     This must be the final step; otherwise we'd have to
+     delete destination variables on failure. */
+  trns->dst_dict = dict;
+  if (trns->src_vars != trns->dst_vars)
+    create_dst_vars (trns, dict);
+
+  /* Done. */
+  add_transformation (ds, &recode_trns_class, trns);
+  return true;
+}
+
 /* Parses the RECODE transformation. */
 int
 cmd_recode (struct lexer *lexer, struct dataset *ds)
 {
   do
     {
-      struct dictionary *dict = dataset_dict (ds);
-      struct recode_trns *trns
-        = pool_create_container (struct recode_trns, pool);
-
-      /* Parse source variable names,
-         then input to output mappings,
-         then destintation variable names. */
-      if (!parse_src_vars (lexer, trns, dict)
-          || !parse_mappings (lexer, trns, dict_get_encoding (dict))
-          || !parse_dst_vars (lexer, trns, dict))
+      struct pool *pool = pool_create ();
+      struct recode_trns *trns = pool_alloc (pool, sizeof *trns);
+      *trns = (struct recode_trns) { .pool = pool };
+
+      if (!parse_one_recoding (lexer, ds, trns))
         {
           recode_trns_free (trns);
           return CMD_FAILURE;
         }
-
-      /* Ensure that all the output strings are at least as wide
-         as the widest destination variable. */
-      if (trns->dst_type == VAL_STRING)
-       {
-         if (! enlarge_dst_widths (trns))
-           {
-             recode_trns_free (trns);
-             return CMD_FAILURE;
-           }
-       }
-
-      /* Create destination variables, if needed.
-         This must be the final step; otherwise we'd have to
-         delete destination variables on failure. */
-      trns->dst_dict = dict;
-      if (trns->src_vars != trns->dst_vars)
-       create_dst_vars (trns, dict);
-
-      /* Done. */
-      add_transformation (ds, &recode_trns_class, trns);
     }
   while (lex_match (lexer, T_SLASH));
 
@@ -203,13 +229,9 @@ static bool
 parse_mappings (struct lexer *lexer, struct recode_trns *trns,
                 const char *dict_encoding)
 {
-  size_t map_allocated;
-  bool have_dst_type;
-  size_t i;
-
   /* Find length of longest source variable. */
   trns->max_src_width = var_get_width (trns->src_vars[0]);
-  for (i = 1; i < trns->n_vars; i++)
+  for (size_t i = 1; i < trns->n_vars; i++)
     {
       size_t var_width = var_get_width (trns->src_vars[i]);
       if (var_width > trns->max_src_width)
@@ -217,10 +239,8 @@ parse_mappings (struct lexer *lexer, struct recode_trns *trns,
     }
 
   /* Parse the mappings in parentheses. */
-  trns->mappings = NULL;
-  trns->n_maps = 0;
-  map_allocated = 0;
-  have_dst_type = false;
+  size_t map_allocated = 0;
+  bool have_dst_type = false;
   if (!lex_force_match (lexer, T_LPAREN))
     return false;
   do
@@ -229,11 +249,7 @@ parse_mappings (struct lexer *lexer, struct recode_trns *trns,
 
       if (!lex_match_id (lexer, "CONVERT"))
         {
-          struct map_out out;
-          size_t first_map_idx;
-          size_t i;
-
-          first_map_idx = trns->n_maps;
+          size_t first_map_idx = trns->n_maps;
 
           /* Parse source specifications. */
           do
@@ -249,42 +265,56 @@ parse_mappings (struct lexer *lexer, struct recode_trns *trns,
             }
           while (!lex_match (lexer, T_EQUALS));
 
+          struct map_out out;
           if (!parse_map_out (lexer, trns->pool, &out))
             return false;
 
-         if (out.copy_input)
-           dst_type = trns->src_type;
-         else
-           dst_type = val_type_from_width (out.width);
-          if (have_dst_type && dst_type != trns->dst_type)
-            {
-              msg (SE, _("Inconsistent target variable types.  "
-                         "Target variables "
-                         "must be all numeric or all string."));
-              return false;
-            }
-
-          for (i = first_map_idx; i < trns->n_maps; i++)
+          dst_type = (out.copy_input
+                      ? trns->src_type
+                      : val_type_from_width (out.width));
+          for (size_t i = first_map_idx; i < trns->n_maps; i++)
             trns->mappings[i].out = out;
         }
       else
         {
           /* Parse CONVERT as a special case. */
-          struct map_in in;
-          set_map_in_generic (&in, MAP_CONVERT);
+          struct map_in in = { .type = MAP_CONVERT };
           add_mapping (trns, &map_allocated, &in);
-          set_map_out_num (&trns->mappings[trns->n_maps - 1].out, 0.0);
+
+          int ofs = lex_ofs (lexer) - 1;
+          trns->mappings[trns->n_maps - 1].out = (struct map_out) {
+            .ofs = ofs,
+          };
 
           dst_type = VAL_NUMERIC;
-          if (trns->src_type != VAL_STRING
-              || (have_dst_type && trns->dst_type != VAL_NUMERIC))
+          if (trns->src_type != VAL_STRING)
             {
-              lex_next_error (lexer, -1, -1,
-                              _("CONVERT requires string input values and "
-                                "numeric output values."));
+              lex_ofs_error (lexer, ofs, ofs,
+                             _("CONVERT requires string input values."));
               return false;
             }
         }
+      if (have_dst_type && dst_type != trns->dst_type)
+        {
+          msg (SE, _("Output values must be all numeric or all string."));
+
+          assert (trns->n_maps > 1);
+          const struct map_out *numeric = &trns->mappings[trns->n_maps - 2].out;
+          const struct map_out *string = &trns->mappings[trns->n_maps - 1].out;
+
+          if (trns->dst_type == VAL_STRING)
+            {
+              const struct map_out *tmp = numeric;
+              numeric = string;
+              string = tmp;
+            }
+
+          lex_ofs_msg (lexer, SN, numeric->ofs, numeric->ofs,
+                       _("This output value is numeric."));
+          lex_ofs_msg (lexer, SN, string->ofs, string->ofs,
+                       _("This output value is string."));
+          return false;
+        }
       trns->dst_type = dst_type;
       have_dst_type = true;
 
@@ -308,25 +338,29 @@ parse_map_in (struct lexer *lexer, struct map_in *in, struct pool *pool,
 {
 
   if (lex_match_id (lexer, "ELSE"))
-    set_map_in_generic (in, MAP_ELSE);
+    *in = (struct map_in) { .type = MAP_ELSE };
   else if (src_type == VAL_NUMERIC)
     {
       if (lex_match_id (lexer, "MISSING"))
-        set_map_in_generic (in, MAP_MISSING);
+        *in = (struct map_in) { .type = MAP_MISSING };
       else if (lex_match_id (lexer, "SYSMIS"))
-        set_map_in_generic (in, MAP_SYSMIS);
+        *in = (struct map_in) { .type = MAP_SYSMIS };
       else
         {
           double x, y;
           if (!parse_num_range (lexer, &x, &y, NULL))
             return false;
-          set_map_in_num (in, x == y ? MAP_SINGLE : MAP_RANGE, x, y);
+          *in = (struct map_in) {
+            .type = x == y ? MAP_SINGLE : MAP_RANGE,
+            .x = { .f = x },
+            .y = { .f = y },
+          };
         }
     }
   else
     {
       if (lex_match_id (lexer, "MISSING"))
-        set_map_in_generic (in, MAP_MISSING);
+        *in = (struct map_in) { .type = MAP_MISSING };
       else if (!lex_force_string (lexer))
         return false;
       else
@@ -334,11 +368,11 @@ parse_map_in (struct lexer *lexer, struct map_in *in, struct pool *pool,
          set_map_in_str (in, pool, lex_tokss (lexer), max_src_width,
                           dict_encoding);
          lex_get (lexer);
-         if (lex_token (lexer) == T_ID
-             && lex_id_match (ss_cstr ("THRU"), lex_tokss (lexer)))
+         if (lex_match_id (lexer, "THRU"))
            {
-             lex_error (lexer, _("%s is not allowed with string variables."),
-                         "THRU");
+             lex_next_error (lexer, -1, -1,
+                              _("%s is not allowed with string variables."),
+                              "THRU");
              return false;
            }
        }
@@ -363,23 +397,6 @@ add_mapping (struct recode_trns *trns,
   m->in = *in;
 }
 
-/* Sets IN as a mapping of the given TYPE. */
-static void
-set_map_in_generic (struct map_in *in, enum map_in_type type)
-{
-  in->type = type;
-}
-
-/* Sets IN as a numeric mapping of the given TYPE,
-   with X and Y as the two numeric values. */
-static void
-set_map_in_num (struct map_in *in, enum map_in_type type, double x, double y)
-{
-  in->type = type;
-  in->x.f = x;
-  in->y.f = y;
-}
-
 /* Sets IN as a string mapping, with STRING as the string,
    allocated from POOL.  The string is padded with spaces on the
    right to WIDTH characters long. */
@@ -388,9 +405,10 @@ set_map_in_str (struct map_in *in, struct pool *pool,
                 struct substring string, size_t width,
                 const char *dict_encoding)
 {
+  *in = (struct map_in) { .type = MAP_SINGLE };
+
   char *s = recode_string (dict_encoding, "UTF-8",
                            ss_data (string), ss_length (string));
-  in->type = MAP_SINGLE;
   value_init_pool (pool, &in->x, width);
   value_copy_buf_rpad (&in->x, width,
                        CHAR_CAST (uint8_t *, s), strlen (s), ' ');
@@ -404,38 +422,27 @@ parse_map_out (struct lexer *lexer, struct pool *pool, struct map_out *out)
 {
   if (lex_is_number (lexer))
     {
-      set_map_out_num (out, lex_number (lexer));
+      *out = (struct map_out) { .value = { .f = lex_number (lexer) } };
       lex_get (lexer);
     }
   else if (lex_match_id (lexer, "SYSMIS"))
-    set_map_out_num (out, SYSMIS);
+    *out = (struct map_out) { .value = { .f = SYSMIS } };
   else if (lex_is_string (lexer))
     {
       set_map_out_str (out, pool, lex_tokss (lexer));
       lex_get (lexer);
     }
   else if (lex_match_id (lexer, "COPY"))
-    {
-      out->copy_input = true;
-      out->width = 0;
-    }
+    *out = (struct map_out) { .copy_input = true };
   else
     {
       lex_error (lexer, _("Syntax error expecting output value."));
       return false;
     }
+  out->ofs = lex_ofs (lexer) - 1;
   return true;
 }
 
-/* Sets OUT as a numeric mapping output with the given VALUE. */
-static void
-set_map_out_num (struct map_out *out, double value)
-{
-  out->copy_input = false;
-  out->value.f = value;
-  out->width = 0;
-}
-
 /* Sets OUT as a string mapping output with the given VALUE. */
 static void
 set_map_out_str (struct map_out *out, struct pool *pool,
@@ -452,82 +459,96 @@ set_map_out_str (struct map_out *out, struct pool *pool,
       length = 1;
     }
 
-  out->copy_input = false;
+  *out = (struct map_out) { .width = length };
   value_init_pool (pool, &out->value, length);
   memcpy (out->value.s, string, length);
-  out->width = length;
 }
 
 /* Parses a set of target variables into TRNS->dst_vars and
    TRNS->dst_names. */
 static bool
 parse_dst_vars (struct lexer *lexer, struct recode_trns *trns,
-               const struct dictionary *dict)
+               const struct dictionary *dict, int src_start, int src_end,
+                int mappings_start, int mappings_end)
 {
-  size_t i;
-
+  int dst_start, dst_end;
   if (lex_match_id (lexer, "INTO"))
     {
+      dst_start = lex_ofs (lexer);
       size_t n_names;
-      size_t i;
-
       if (!parse_mixed_vars_pool (lexer, dict, trns->pool,
                                  &trns->dst_names, &n_names,
                                   PV_NONE))
         return false;
+      dst_end = lex_ofs (lexer) - 1;
 
       if (n_names != trns->n_vars)
         {
-          msg (SE, _("%zu variable(s) cannot be recoded into "
-                     "%zu variable(s).  Specify the same number "
-                     "of variables as source and target variables."),
-               trns->n_vars, n_names);
+          msg (SE, _("Source and target variable counts must match."));
+          lex_ofs_msg (lexer, SN, src_start, src_end,
+                       ngettext ("There is %zu source variable.",
+                                 "There are %zu source variables.",
+                                 trns->n_vars),
+                       trns->n_vars);
+          lex_ofs_msg (lexer, SN, dst_start, dst_end,
+                       ngettext ("There is %zu target variable.",
+                                 "There are %zu target variables.",
+                                 n_names),
+                       n_names);
           return false;
         }
 
       trns->dst_vars = pool_nalloc (trns->pool,
                                     trns->n_vars, sizeof *trns->dst_vars);
-      for (i = 0; i < trns->n_vars; i++)
+      for (size_t i = 0; i < trns->n_vars; i++)
         {
           const struct variable *v;
           v = trns->dst_vars[i] = dict_lookup_var (dict, trns->dst_names[i]);
           if (v == NULL && trns->dst_type == VAL_STRING)
             {
-              msg (SE, _("There is no variable named "
-                         "%s.  (All string variables specified "
-                         "on INTO must already exist.  Use the "
-                         "STRING command to create a string "
-                         "variable.)"),
-                   trns->dst_names[i]);
+              msg (SE, _("All string variables specified on INTO must already "
+                         "exist.  (Use the STRING command to create a string "
+                         "variable.)"));
+              lex_ofs_msg (lexer, SN, dst_start, dst_end,
+                           _("There is no variable named %s."),
+                           trns->dst_names[i]);
               return false;
             }
         }
-
     }
   else
     {
+      dst_start = src_start;
+      dst_end = src_end;
+
       trns->dst_vars = trns->src_vars;
       if (trns->src_type != trns->dst_type)
         {
-          msg (SE, _("INTO is required with %s input values "
-                     "and %s output values."),
-               trns->src_type == VAL_NUMERIC ? _("numeric") : _("string"),
-               trns->dst_type == VAL_NUMERIC ? _("numeric") : _("string"));
+          if (trns->src_type == VAL_NUMERIC)
+            lex_ofs_error (lexer, mappings_start, mappings_end,
+                           _("INTO is required with numeric input values "
+                             "and string output values."));
+          else
+            lex_ofs_error (lexer, mappings_start, mappings_end,
+                           _("INTO is required with string input values "
+                             "and numeric output values."));
           return false;
         }
     }
 
-  for (i = 0; i < trns->n_vars; i++)
+  for (size_t i = 0; i < trns->n_vars; i++)
     {
       const struct variable *v = trns->dst_vars[i];
-      if (v != NULL && var_get_type (v) != trns->dst_type)
+      if (v && var_get_type (v) != trns->dst_type)
         {
           if (trns->dst_type == VAL_STRING)
-            msg (SE, _("Type mismatch.  Cannot store string data in "
-                       "numeric variable %s."), var_get_name (v));
+            lex_ofs_error (lexer, dst_start, dst_end,
+                           _("Type mismatch: cannot store string data in "
+                             "numeric variable %s."), var_get_name (v));
           else
-            msg (SE, _("Type mismatch.  Cannot store numeric data in "
-                       "string variable %s."), var_get_name (v));
+            lex_ofs_error (lexer, dst_start, dst_end,
+                           _("Type mismatch: cannot store numeric data in "
+                             "string variable %s."), var_get_name (v));
           return false;
         }
     }
@@ -538,14 +559,14 @@ parse_dst_vars (struct lexer *lexer, struct recode_trns *trns,
 /* Ensures that all the output values in TRNS are as wide as the
    widest destination variable. */
 static bool
-enlarge_dst_widths (struct recode_trns *trns)
+enlarge_dst_widths (struct lexer *lexer, struct recode_trns *trns,
+                    int dst_start, int dst_end)
 {
-  size_t i;
   const struct variable *narrow_var = NULL;
   int min_dst_width = INT_MAX;
   trns->max_dst_width = 0;
 
-  for (i = 0; i < trns->n_vars; i++)
+  for (size_t i = 0; i < trns->n_vars; i++)
     {
       const struct variable *v = trns->dst_vars[i];
       if (var_get_width (v) > trns->max_dst_width)
@@ -558,16 +579,22 @@ enlarge_dst_widths (struct recode_trns *trns)
        }
     }
 
-  for (i = 0; i < trns->n_maps; i++)
+  for (size_t i = 0; i < trns->n_maps; i++)
     {
       struct map_out *out = &trns->mappings[i].out;
       if (!out->copy_input)
        {
          if (out->width > min_dst_width)
            {
-             msg (ME,
-                  _("Cannot recode because the variable %s would require a width of %d bytes or greater, but it has a width of only %d bytes."),
-                  var_get_name (narrow_var), out->width, min_dst_width);
+              msg (SE, _("At least one target variable is too narrow for "
+                         "the output values."));
+              lex_ofs_msg (lexer, SN, out->ofs, out->ofs,
+                           _("This recoding output value has width %d."),
+                           out->width);
+              lex_ofs_msg (lexer, SN, dst_start, dst_end,
+                           _("Target variable %s only has width %d."),
+                           var_get_name (narrow_var),
+                           var_get_width (narrow_var));
              return false;
            }
 
@@ -583,9 +610,7 @@ enlarge_dst_widths (struct recode_trns *trns)
 static void
 create_dst_vars (struct recode_trns *trns, struct dictionary *dict)
 {
-  size_t i;
-
-  for (i = 0; i < trns->n_vars; i++)
+  for (size_t i = 0; i < trns->n_vars; i++)
     {
       const struct variable **var = &trns->dst_vars[i];
       const char *name = trns->dst_names[i];
@@ -604,9 +629,8 @@ create_dst_vars (struct recode_trns *trns, struct dictionary *dict)
 static const struct map_out *
 find_src_numeric (struct recode_trns *trns, double value, const struct variable *v)
 {
-  struct mapping *m;
-
-  for (m = trns->mappings; m < trns->mappings + trns->n_maps; m++)
+  for (struct mapping *m = trns->mappings; m < trns->mappings + trns->n_maps;
+       m++)
     {
       const struct map_in *in = &m->in;
       const struct map_out *out = &m->out;
@@ -648,9 +672,8 @@ find_src_string (struct recode_trns *trns, const uint8_t *value,
 {
   const char *encoding = dict_get_encoding (trns->dst_dict);
   int width = var_get_width (src_var);
-  struct mapping *m;
-
-  for (m = trns->mappings; m < trns->mappings + trns->n_maps; m++)
+  for (struct mapping *m = trns->mappings; m < trns->mappings + trns->n_maps;
+       m++)
     {
       const struct map_in *in = &m->in;
       struct map_out *out = &m->out;
@@ -697,10 +720,9 @@ static enum trns_result
 recode_trns_proc (void *trns_, struct ccase **c, casenumber case_idx UNUSED)
 {
   struct recode_trns *trns = trns_;
-  size_t i;
 
   *c = case_unshare (*c);
-  for (i = 0; i < trns->n_vars; i++)
+  for (size_t i = 0; i < trns->n_vars; i++)
     {
       const struct variable *src_var = trns->src_vars[i];
       const struct variable *dst_var = trns->dst_vars[i];
index bab4f9c1865a158d78bd32b72d55a7f920810f53..706b57a09341363028c909524cdb81e29c7f796c 100644 (file)
@@ -1021,4 +1021,4 @@ AT_CHECK([pspp -O format=csv t-test.sps], [1], [dnl
    21 | T-TEST MISSING=INCLUDE/TESTVAL=1.
       | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
 ])
-AT_CLEANUP
\ No newline at end of file
+AT_CLEANUP
index 9927d3c3d5940534f676b9527d66d4d030d4cd51..f12185a410492f826dfd8b67fdfbd15a6b909253 100644 (file)
@@ -330,7 +330,15 @@ list.
 ])
 
 AT_CHECK([pspp -O format=csv recode.sps], [1], [dnl
-"error: Cannot recode because the variable x would require a width of 5 bytes or greater, but it has a width of only 1 bytes."
+recode.sps:9: error: RECODE: At least one target variable is too narrow for the output values.
+
+"recode.sps:9.19-9.25: note: RECODE: This recoding output value has width 5.
+    9 | recode x y (""a"" = ""first"") .
+      |                   ^~~~~~~"
+
+"recode.sps:9.8-9.10: note: RECODE: Target variable x only has width 1.
+    9 | recode x y (""a"" = ""first"") .
+      |        ^~~"
 
 Table: Data List
 x,y,z
@@ -358,8 +366,144 @@ END DATA.
 
 RECODE x (1=9) INTO ".
 EXECUTE.
+dnl "
 ])
 
 AT_CHECK([pspp -O format=csv recode.sps], [1], [ignore])
 
 AT_CLEANUP
+
+
+AT_SETUP([RECODE syntax errors])
+AT_DATA([recode.sps], [dnl
+DATA LIST LIST NOTABLE/n1 to n4 (F8.0) s1 (A1) s2 (A2) s3 (A3) s4 (A4).
+RECODE **.
+RECODE n1 **.
+RECODE n1(**).
+RECODE s1(**).
+RECODE s1('x' THRU 'y').
+RECODE n1(1=**).
+RECODE n1(CONVERT).
+RECODE n1(1=2)(1='x').
+RECODE n1(1='x')(1=2).
+RECODE s1(CONVERT)('1'='1').
+RECODE n1(1=2 **).
+RECODE n1(CONVERT).
+RECODE s1(CONVERT) INTO n1 n2.
+RECODE n1(1='1') INTO xyzzy.
+RECODE n1(1='1').
+RECODE s1('1'=1).
+RECODE n1(1='1') INTO n2.
+RECODE s1(CONVERT) INTO s2.
+RECODE n1 TO n4(1='123456') INTO s1 TO s4.
+])
+AT_CHECK([pspp -O format=csv recode.sps], [1], [dnl
+"recode.sps:2.8-2.9: error: RECODE: Syntax error expecting variable name.
+    2 | RECODE **.
+      |        ^~"
+
+"recode.sps:3.11-3.12: error: RECODE: Syntax error expecting `('.
+    3 | RECODE n1 **.
+      |           ^~"
+
+"recode.sps:4.11-4.12: error: RECODE: Syntax error expecting number.
+    4 | RECODE n1(**).
+      |           ^~"
+
+"recode.sps:5.11-5.12: error: RECODE: Syntax error expecting string.
+    5 | RECODE s1(**).
+      |           ^~"
+
+"recode.sps:6.15-6.18: error: RECODE: THRU is not allowed with string variables.
+    6 | RECODE s1('x' THRU 'y').
+      |               ^~~~"
+
+"recode.sps:7.13-7.14: error: RECODE: Syntax error expecting output value.
+    7 | RECODE n1(1=**).
+      |             ^~"
+
+"recode.sps:8.11-8.17: error: RECODE: CONVERT requires string input values.
+    8 | RECODE n1(CONVERT).
+      |           ^~~~~~~"
+
+recode.sps:9: error: RECODE: Output values must be all numeric or all string.
+
+"recode.sps:9.13: note: RECODE: This output value is numeric.
+    9 | RECODE n1(1=2)(1='x').
+      |             ^"
+
+"recode.sps:9.18-9.20: note: RECODE: This output value is string.
+    9 | RECODE n1(1=2)(1='x').
+      |                  ^~~"
+
+recode.sps:10: error: RECODE: Output values must be all numeric or all string.
+
+"recode.sps:10.20: note: RECODE: This output value is numeric.
+   10 | RECODE n1(1='x')(1=2).
+      |                    ^"
+
+"recode.sps:10.13-10.15: note: RECODE: This output value is string.
+   10 | RECODE n1(1='x')(1=2).
+      |             ^~~"
+
+recode.sps:11: error: RECODE: Output values must be all numeric or all string.
+
+"recode.sps:11.11-11.17: note: RECODE: This output value is numeric.
+   11 | RECODE s1(CONVERT)('1'='1').
+      |           ^~~~~~~"
+
+"recode.sps:11.24-11.26: note: RECODE: This output value is string.
+   11 | RECODE s1(CONVERT)('1'='1').
+      |                        ^~~"
+
+"recode.sps:12.15-12.16: error: RECODE: Syntax error expecting `)'.
+   12 | RECODE n1(1=2 **).
+      |               ^~"
+
+"recode.sps:13.11-13.17: error: RECODE: CONVERT requires string input values.
+   13 | RECODE n1(CONVERT).
+      |           ^~~~~~~"
+
+recode.sps:14: error: RECODE: Source and target variable counts must match.
+
+"recode.sps:14.8-14.9: note: RECODE: There is 1 source variable.
+   14 | RECODE s1(CONVERT) INTO n1 n2.
+      |        ^~"
+
+"recode.sps:14.25-14.29: note: RECODE: There are 2 target variables.
+   14 | RECODE s1(CONVERT) INTO n1 n2.
+      |                         ^~~~~"
+
+recode.sps:15: error: RECODE: All string variables specified on INTO must already exist.  (Use the STRING command to create a string variable.)
+
+"recode.sps:15.23-15.27: note: RECODE: There is no variable named xyzzy.
+   15 | RECODE n1(1='1') INTO xyzzy.
+      |                       ^~~~~"
+
+"recode.sps:16.10-16.16: error: RECODE: INTO is required with numeric input values and string output values.
+   16 | RECODE n1(1='1').
+      |          ^~~~~~~"
+
+"recode.sps:17.10-17.16: error: RECODE: INTO is required with string input values and numeric output values.
+   17 | RECODE s1('1'=1).
+      |          ^~~~~~~"
+
+"recode.sps:18.23-18.24: error: RECODE: Type mismatch: cannot store string data in numeric variable n2.
+   18 | RECODE n1(1='1') INTO n2.
+      |                       ^~"
+
+"recode.sps:19.25-19.26: error: RECODE: Type mismatch: cannot store numeric data in string variable s2.
+   19 | RECODE s1(CONVERT) INTO s2.
+      |                         ^~"
+
+recode.sps:20: error: RECODE: At least one target variable is too narrow for the output values.
+
+"recode.sps:20.19-20.26: note: RECODE: This recoding output value has width 6.
+   20 | RECODE n1 TO n4(1='123456') INTO s1 TO s4.
+      |                   ^~~~~~~~"
+
+"recode.sps:20.29-20.41: note: RECODE: Target variable s1 only has width 1.
+   20 | RECODE n1 TO n4(1='123456') INTO s1 TO s4.
+      |                             ^~~~~~~~~~~~~"
+])
+AT_CLEANUP
\ No newline at end of file