Better errors for odd-length keyword args
authorAndy Wingo <wingo@pobox.com>
Tue, 28 Feb 2017 19:42:45 +0000 (20:42 +0100)
committerAndy Wingo <wingo@pobox.com>
Tue, 28 Feb 2017 21:01:20 +0000 (22:01 +0100)
* libguile/vm-engine.c (bind-kwargs):
* libguile/vm.c (vm_error_kwargs_missing_value):
* libguile/eval.c (error_missing_value)
  (prepare_boot_closure_env_for_apply): Adapt to mirror VM behavior.
* libguile/keywords.c (scm_c_bind_keyword_arguments): Likewise.
* module/ice-9/eval.scm (primitive-eval): Update to error on (foo #:kw)
  with a "Keyword argument has no value" instead of the horrible "odd
  argument list length".  Also adapts to the expected args format for
  the keyword-argument-error exception printer in all cases.  Matches
  1.8 optargs behavior also.
* test-suite/standalone/test-scm-c-bind-keyword-arguments.c (test_missing_value):
  (missing_value_error_handler): Update test.
* test-suite/tests/optargs.test: Add tests.

libguile/eval.c
libguile/keywords.c
libguile/vm-engine.c
libguile/vm.c
module/ice-9/eval.scm
test-suite/standalone/test-scm-c-bind-keyword-arguments.c
test-suite/tests/optargs.test

index 93788ebfcb35076da886bbdd9b5218517204db14..e9ff02a8b0bbbde99cc39cdca9062739f6cd9ef7 100644 (file)
@@ -195,6 +195,12 @@ env_set (SCM env, int depth, int width, SCM val)
   VECTOR_SET (env, width + 1, val);
 }
 
+static void error_missing_value (SCM proc, SCM kw)
+{
+  scm_error_scm (scm_from_latin1_symbol ("keyword-argument-error"), proc,
+                 scm_from_locale_string ("Keyword argument has no value"), SCM_EOL,
+                 scm_list_1 (kw));
+}
 
 static void error_invalid_keyword (SCM proc, SCM obj)
 {
@@ -832,28 +838,40 @@ prepare_boot_closure_env_for_apply (SCM proc, SCM args,
           {
             SCM walk;
 
-            if (scm_is_pair (args) && scm_is_pair (CDR (args)))
-              for (; scm_is_pair (args) && scm_is_pair (CDR (args));
-                   args = CDR (args))
-                {
-                  SCM k = CAR (args), v = CADR (args);
-                  if (!scm_is_keyword (k))
+            while (scm_is_pair (args))
+              {
+                SCM k = CAR (args);
+                args = CDR (args);
+                if (!scm_is_keyword (k))
+                  {
+                    if (scm_is_true (rest))
+                      continue;
+                    else
+                      break;
+                  }
+                for (walk = kw; scm_is_pair (walk); walk = CDR (walk))
+                  if (scm_is_eq (k, CAAR (walk)))
                     {
-                      if (scm_is_true (rest))
-                        continue;
+                      if (scm_is_pair (args))
+                        {
+                          SCM v = CAR (args);
+                          args = CDR (args);
+                          env_set (env, 0, SCM_I_INUM (CDAR (walk)), v);
+                          break;
+                        }
                       else
-                        break;
+                        error_missing_value (proc, k);
                     }
-                  for (walk = kw; scm_is_pair (walk); walk = CDR (walk))
-                    if (scm_is_eq (k, CAAR (walk)))
-                      {
-                        env_set (env, 0, SCM_I_INUM (CDAR (walk)), v);
-                        args = CDR (args);
-                        break;
-                      }
-                  if (scm_is_null (walk) && scm_is_false (aok))
-                    error_unrecognized_keyword (proc, k);
-                }
+                if (scm_is_null (walk))
+                  {
+                    if (scm_is_false (aok))
+                      error_unrecognized_keyword (proc, k);
+                    else if (!scm_is_pair (args))
+                      /* Advance past argument of unrecognized
+                         keyword, if present.  */
+                      args = CDR (args);
+                  }
+              }
             if (scm_is_pair (args) && scm_is_false (rest))
               error_invalid_keyword (proc, CAR (args));
           }
index 0ead3369254c999e8a654c3b8e9e5c3c14070564..087042b842c1598c6bd7c6bdb6b78fefe5393cb8 100644 (file)
@@ -125,18 +125,12 @@ scm_c_bind_keyword_arguments (const char *subr, SCM rest,
 {
   va_list va;
 
-  if (SCM_UNLIKELY (!(flags & SCM_ALLOW_NON_KEYWORD_ARGUMENTS)
-                    && scm_ilength (rest) % 2 != 0))
-    scm_error (scm_keyword_argument_error,
-               subr, "Odd length of keyword argument list",
-               SCM_EOL, SCM_BOOL_F);
-
   while (scm_is_pair (rest))
     {
       SCM kw_or_arg = SCM_CAR (rest);
       SCM tail = SCM_CDR (rest);
 
-      if (scm_is_keyword (kw_or_arg) && scm_is_pair (tail))
+      if (scm_is_keyword (kw_or_arg))
         {
           SCM kw;
           SCM *arg_p;
@@ -154,6 +148,11 @@ scm_c_bind_keyword_arguments (const char *subr, SCM rest,
                                   scm_from_latin1_string
                                   ("Unrecognized keyword"),
                                   SCM_EOL, scm_list_1 (kw_or_arg));
+
+                  /* Advance REST.  Advance past the argument of an
+                     unrecognized keyword, but don't error if such a
+                     keyword has no argument.  */
+                  rest = scm_is_pair (tail) ? SCM_CDR (tail) : tail;
                   break;
                 }
               arg_p = va_arg (va, SCM *);
@@ -161,14 +160,19 @@ scm_c_bind_keyword_arguments (const char *subr, SCM rest,
                 {
                   /* We found the matching keyword.  Store the
                      associated value and break out of the loop.  */
+                  if (!scm_is_pair (tail))
+                    scm_error_scm (scm_keyword_argument_error,
+                                  scm_from_locale_string (subr),
+                                  scm_from_latin1_string
+                                  ("Keyword argument has no value"),
+                                  SCM_EOL, scm_list_1 (kw));
                   *arg_p = SCM_CAR (tail);
+                  /* Advance REST.  */
+                  rest = SCM_CDR (tail);
                   break;
                 }
             }
           va_end (va);
-
-          /* Advance REST.  */
-          rest = SCM_CDR (tail);
         }
       else
         {
index c9a9cecd16ef2ec9878bf12270b6a89f54fda314..9ddda8f2a32e513d845a856fead83e719d3f16f1 100644 (file)
@@ -1269,9 +1269,6 @@ VM_NAME (scm_i_thread *thread, struct scm_vm *vp,
       while (n < ntotal)
         FP_SET (n++, SCM_UNDEFINED);
 
-      VM_ASSERT (has_rest || (nkw % 2) == 0,
-                 vm_error_kwargs_length_not_even (FP_REF (0)));
-
       /* Now bind keywords, in the order given.  */
       for (n = 0; n < nkw; n++)
         if (scm_is_keyword (FP_REF (ntotal + n)))
@@ -1281,8 +1278,14 @@ VM_NAME (scm_i_thread *thread, struct scm_vm *vp,
               if (scm_is_eq (SCM_CAAR (walk), FP_REF (ntotal + n)))
                 {
                   SCM si = SCM_CDAR (walk);
-                  FP_SET (SCM_I_INUMP (si) ? SCM_I_INUM (si) : scm_to_uint32 (si),
-                          FP_REF (ntotal + n + 1));
+                  if (n + 1 < nkw)
+                    {
+                      FP_SET (SCM_I_INUMP (si) ? SCM_I_INUM (si) : scm_to_uint32 (si),
+                              FP_REF (ntotal + n + 1));
+                    }
+                  else
+                    vm_error_kwargs_missing_value (FP_REF (0),
+                                                   FP_REF (ntotal + n));
                   break;
                 }
             VM_ASSERT (scm_is_pair (walk) || allow_other_keys,
index be30517c505691685c0d9ccf2dfa4ff7c76ffafd..e8f75b14f25c93e308aa4619f4ee1392c64a3e7c 100644 (file)
@@ -422,7 +422,7 @@ static void vm_error_bad_instruction (scm_t_uint32 inst) SCM_NORETURN SCM_NOINLI
 static void vm_error_unbound (SCM sym) SCM_NORETURN SCM_NOINLINE;
 static void vm_error_not_a_variable (const char *func_name, SCM x) SCM_NORETURN SCM_NOINLINE;
 static void vm_error_apply_to_non_list (SCM x) SCM_NORETURN SCM_NOINLINE;
-static void vm_error_kwargs_length_not_even (SCM proc) SCM_NORETURN SCM_NOINLINE;
+static void vm_error_kwargs_missing_value (SCM proc, SCM kw) SCM_NORETURN SCM_NOINLINE;
 static void vm_error_kwargs_invalid_keyword (SCM proc, SCM obj) SCM_NORETURN SCM_NOINLINE;
 static void vm_error_kwargs_unrecognized_keyword (SCM proc, SCM kw) SCM_NORETURN SCM_NOINLINE;
 static void vm_error_wrong_num_args (SCM proc) SCM_NORETURN SCM_NOINLINE;
@@ -479,11 +479,11 @@ vm_error_apply_to_non_list (SCM x)
 }
 
 static void
-vm_error_kwargs_length_not_even (SCM proc)
+vm_error_kwargs_missing_value (SCM proc, SCM kw)
 {
   scm_error_scm (sym_keyword_argument_error, proc,
-                 scm_from_latin1_string ("Odd length of keyword argument list"),
-                 SCM_EOL, SCM_BOOL_F);
+                 scm_from_latin1_string ("Keyword argument has no value"),
+                 SCM_EOL, scm_list_1 (kw));
 }
 
 static void
index a2bab2065c4ad243828eced730b23681872437a7..d21f59abde9ca33564db22f9cf45c91395c28ef5 100644 (file)
               (define (bind-kw args)
                 (let lp ((args args))
                   (cond
-                   ((and (pair? args) (pair? (cdr args))
-                         (keyword? (car args)))
-                    (let ((kw-pair (assq (car args) keywords))
-                          (v (cadr args)))
-                      (if kw-pair
-                          ;; Found a known keyword; set its value.
-                          (env-set! env 0 (cdr kw-pair) v)
-                          ;; Unknown keyword.
-                          (if (not allow-other-keys?)
-                              ((scm-error
-                                'keyword-argument-error
-                                "eval" "Unrecognized keyword"
-                                '() (list (car args))))))
-                      (lp (cddr args))))
                    ((pair? args)
-                    (if rest?
-                        ;; Be lenient parsing rest args.
-                        (lp (cdr args))
-                        ((scm-error 'keyword-argument-error
-                                    "eval" "Invalid keyword"
-                                    '() (list (car args))))))
+                    (cond
+                     ((keyword? (car args))
+                      (let ((k (car args))
+                            (args (cdr args)))
+                        (cond
+                         ((assq k keywords)
+                          => (lambda (kw-pair)
+                               ;; Found a known keyword; set its value.
+                               (if (pair? args)
+                                   (let ((v (car args))
+                                         (args (cdr args)))
+                                     (env-set! env 0 (cdr kw-pair) v)
+                                     (lp args))
+                                   ((scm-error 'keyword-argument-error
+                                               "eval"
+                                               "Keyword argument has no value"
+                                               '() (list k))))))
+                         ;; Otherwise unknown keyword.
+                         (allow-other-keys?
+                          (lp (if (pair? args) (cdr args) args)))
+                         (else
+                          ((scm-error 'keyword-argument-error
+                                      "eval" "Unrecognized keyword"
+                                      '() (list k)))))))
+                     (rest?
+                      ;; Be lenient parsing rest args.
+                      (lp (cdr args)))
+                     (else
+                      ((scm-error 'keyword-argument-error
+                                  "eval" "Invalid keyword"
+                                  '() (list (car args)))))))
                    (else
                     (body env)))))
               (bind-req args))))))))
index f4cd53d8446e93fe474d92660a5dcd170fcc9405..90bcf2bafdea0764030a3825a2d66fac2e9d0dea 100644 (file)
@@ -94,33 +94,31 @@ invalid_keyword_error_handler (void *data, SCM key, SCM args)
 }
 
 static SCM
-test_odd_length (void *data)
+test_missing_value (void *data)
 {
   SCM k_foo = scm_from_utf8_keyword ("foo");
-  SCM k_bar = scm_from_utf8_keyword ("bar");
-  SCM arg_foo, arg_bar;
+  SCM arg_foo;
 
   scm_c_bind_keyword_arguments ("test",
-                                scm_list_n (k_foo, SCM_EOL,
-                                            SCM_INUM0,
+                                scm_list_n (k_foo,
                                             SCM_UNDEFINED),
                                 SCM_ALLOW_OTHER_KEYS,
                                 k_foo, &arg_foo,
-                                k_bar, &arg_bar,
                                 SCM_UNDEFINED);
   assert (0);
 }
 
 static SCM
-odd_length_error_handler (void *data, SCM key, SCM args)
+missing_value_error_handler (void *data, SCM key, SCM args)
 {
   SCM expected_args = scm_list_n
     (scm_from_utf8_string ("test"),
-     scm_from_utf8_string ("Odd length of keyword argument list"),
-     SCM_EOL, SCM_BOOL_F,
+     scm_from_utf8_string ("Keyword argument has no value"),
+     SCM_EOL, scm_list_1 (scm_from_utf8_keyword ("foo")),
      SCM_UNDEFINED);
 
   assert (scm_is_eq (key, scm_from_utf8_symbol ("keyword-argument-error")));
+  scm_write (args, scm_current_output_port ());
   assert (scm_is_true (scm_equal_p (args, expected_args)));
 
   return SCM_BOOL_T;
@@ -214,10 +212,10 @@ test_scm_c_bind_keyword_arguments ()
                       test_invalid_keyword, NULL,
                       invalid_keyword_error_handler, NULL);
 
-  /* Test odd length error.  */
+  /* Test missing value error.  */
   scm_internal_catch (SCM_BOOL_T,
-                      test_odd_length, NULL,
-                      odd_length_error_handler, NULL);
+                      test_missing_value, NULL,
+                      missing_value_error_handler, NULL);
 }
 
 static void
index 047417b4c8ef73a9ee05182d4153270b85d71101..9590f414cbd1c3cfec05cc30ecd8a5c577b92191 100644 (file)
       (lambda (key proc fmt args data)
         data)))
 
+  (pass-if-equal "missing argument" '("Keyword argument has no value" #:x)
+    (catch 'keyword-argument-error
+      (lambda ()
+        (let ((f (lambda* (#:key x) x)))
+          (f #:x)))
+      (lambda (key proc fmt args data)
+        (cons fmt data))))
+
   (pass-if-equal "invalid keyword" '(not-a-keyword)
     (catch 'keyword-argument-error
       (lambda ()
       (lambda (key proc fmt args data)
         data)))
 
+  (pass-if-equal "missing argument"
+      '("Keyword argument has no value" #:encoding)
+    (catch 'keyword-argument-error
+      (lambda ()
+        (open-file "/dev/null" "r" #:encoding))
+      (lambda (key proc fmt args data)
+        (cons fmt data))))
+
   (pass-if-equal "invalid keyword" '(not-a-keyword)
     (catch 'keyword-argument-error
       (lambda ()