Improve conformance with CLHS 7.6.5

Prompted by a report by Daniel Kochmański: rework the keyword argument
checking in effective methods.  The checking needs to be inserted by
the caller of compute-effective-method; it should be orthogonal to the
effective method computation.  The fact that SBCL elides the checking in
the dispatch function, instead relying on the effective method
function to perform the check, is an implementation detail, and the
presence of the local pseudo-macro in the standard effective method
causes problems to particular custom uses of the MOP, for example the
very simple implementation of COMPUTE-DISCRIMINATING-FUNCTION in the
new test file.

As a (very welcome) side effect, this fixes the fact that we
apparently did not perform keyword applicability checking if there
were applicable auxiliary methods on the generic function.
This commit is contained in:
Christophe Rhodes 2023-10-22 14:30:25 +01:00
parent 29e2131406
commit ba76af0b22
5 changed files with 152 additions and 23 deletions

6
NEWS
View file

@ -5,6 +5,12 @@ changes relative to sbcl-2.3.9:
on all safety levels, not just those which it can prove are of sub-page on all safety levels, not just those which it can prove are of sub-page
sizes. It can do this because it now inserts code to check for stack sizes. It can do this because it now inserts code to check for stack
overflow explicitly on higher safety levels. overflow explicitly on higher safety levels.
* bug fix: calls to generic functions now detect erroneous keywords (in the
sense of CLHS 7.6.5) passed as arguments even when auxiliary methods are
applicable.
* bug fix: the standard method on SB-MOP:COMPUTE-EFFECTIVE-METHOD no longer
inserts calls to implementation-defined local macros. (reported by Daniel
Kochmański)
changes in sbcl-2.3.9 relative to sbcl-2.3.8: changes in sbcl-2.3.9 relative to sbcl-2.3.8:
* enhancement: stack allocation via DYNAMIC-EXTENT now applies to all values * enhancement: stack allocation via DYNAMIC-EXTENT now applies to all values

View file

@ -55,7 +55,8 @@
(defun make-effective-method-function1 (generic-function form (defun make-effective-method-function1 (generic-function form
method-alist-p wrappers-p) method-alist-p wrappers-p)
(if (and (listp form) (if (and (listp form)
(eq (car form) 'call-method)) (eq (car form) 'call-method)
(not (gf-requires-emf-keyword-checks generic-function)))
(make-effective-method-function-simple generic-function form) (make-effective-method-function-simple generic-function form)
;; We have some sort of `real' effective method. Go off and get a ;; We have some sort of `real' effective method. Go off and get a
;; compiled function for it. Most of the real hair here is done by ;; compiled function for it. Most of the real hair here is done by
@ -342,7 +343,6 @@
generic-function form method-alist-p wrappers-p) generic-function form method-alist-p wrappers-p)
(fast-method-call '.fast-call-method-list.) (fast-method-call '.fast-call-method-list.)
(t '.call-method-list.))) (t '.call-method-list.)))
(check-applicable-keywords 'check-applicable-keywords)
(t (default-test-converter form)))) (t (default-test-converter form))))
;;; CMUCL comment (2003-10-15): ;;; CMUCL comment (2003-10-15):
@ -368,12 +368,6 @@
(values `(dolist (emf ,gensym nil) (values `(dolist (emf ,gensym nil)
,(make-emf-call (length metatypes) applyp 'emf type)) ,(make-emf-call (length metatypes) applyp 'emf type))
(list gensym)))) (list gensym))))
(check-applicable-keywords
(values `(check-applicable-keywords .keyargs-start.
.valid-keys.
.dfun-more-context.
.dfun-more-count.)
'()))
(t (t
(default-code-converter form)))) (default-code-converter form))))
@ -389,8 +383,6 @@
(make-effective-method-function-simple (make-effective-method-function-simple
generic-function form)) generic-function form))
(cdr form))))) (cdr form)))))
(check-applicable-keywords
'())
(t (t
(default-constant-converter form)))) (default-constant-converter form))))
@ -481,17 +473,12 @@
;; perform this checking in fast-method-functions given ;; perform this checking in fast-method-functions given
;; that they are not solely used for effective method ;; that they are not solely used for effective method
;; functions, but also in combination, when they should not ;; functions, but also in combination, when they should not
;; perform argument checks. ;; perform argument checks. We still return the bare
(let ((call-method ;; CALL-METHOD, but the caller is responsible for ensuring
`(call-method ,(first (primary)) ,(rest (primary))))) ;; that keyword applicability is checked if this is a fast
(if (gf-requires-emf-keyword-checks generic-function) ;; method function used in an effective method. (See
(multiple-value-bind (valid-keys keyargs-start) ;; WRAP-WITH-APPLICABLE-KEYWORD-CHECK below).
(compute-applicable-keywords generic-function applicable-methods) `(call-method ,(first (primary)) ,(rest (primary))))
`(let ((.valid-keys. ',valid-keys)
(.keyargs-start. ',keyargs-start))
(check-applicable-keywords)
,call-method))
call-method)))
(t (t
(let ((main-effective-method (let ((main-effective-method
(if (or (before) (after)) (if (or (before) (after))
@ -636,6 +623,14 @@
((eq t valid-keys)) ((eq t valid-keys))
((not (memq key valid-keys)) (invalid key)))) ((not (memq key valid-keys)) (invalid key))))
(incf i)))))) (incf i))))))
(defun wrap-with-applicable-keyword-check (effective valid-keys keyargs-start)
(setf effective
`(let ((.valid-keys. ',valid-keys)
(.keyargs-start. ',keyargs-start))
(check-applicable-keywords
.keyargs-start. .valid-keys. .dfun-more-context. .dfun-more-count.)
,effective)))
;;;; the STANDARD method combination type. This is coded by hand ;;;; the STANDARD method combination type. This is coded by hand
;;;; (rather than with DEFINE-METHOD-COMBINATION) for bootstrapping ;;;; (rather than with DEFINE-METHOD-COMBINATION) for bootstrapping

View file

@ -1680,6 +1680,10 @@ Except see also BREAK-VICIOUS-METACIRCLE. -- CSR, 2003-05-28
((eq **boot-state** 'complete) ((eq **boot-state** 'complete)
(let* ((combin (generic-function-method-combination gf)) (let* ((combin (generic-function-method-combination gf))
(effective (compute-effective-method gf combin methods))) (effective (compute-effective-method gf combin methods)))
(when (gf-requires-emf-keyword-checks gf)
(multiple-value-bind (valid-keys keyargs-start)
(compute-applicable-keywords gf methods)
(setf effective (wrap-with-applicable-keyword-check effective valid-keys keyargs-start))))
(make-effective-method-function1 (make-effective-method-function1
gf effective method-alist-p wrappers-p))) gf effective method-alist-p wrappers-p)))
((eq (generic-function-name gf) 'make-specializer-form-using-class) ((eq (generic-function-name gf) 'make-specializer-form-using-class)

View file

@ -1289,8 +1289,55 @@
(assert-error (eqls1760987 3 :k2 5) program-error) (assert-error (eqls1760987 3 :k2 5) program-error)
(assert-error (eqls1760987 3 :k1 2 :k2 5) program-error) (assert-error (eqls1760987 3 :k1 2 :k2 5) program-error)
(assert-error (eqls1760987 3 :k4 2 :k2 5) program-error) (assert-error (eqls1760987 3 :k4 2 :k2 5) program-error)
(assert-error (eqls1760987 3 :k1 2 :k3 3 :k2 5) program-error) (assert-error (eqls1760987 3 :k1 2 :k3 3 :k2 5) program-error))
)
;;; CLHS 7.6.5 should still hold in the presence of auxiliary methods
(defgeneric gf-with-keys-to-check (a &key b)
(:method ((a integer) &key b) (declare (ignore b)) (1+ a))
(:method ((a string) &key b) (list a b))
(:method ((a symbol) &key b c) (declare (ignore b)) (list a c))
(:method :around ((a integer) &key b) (declare (ignore b)) (1+ (call-next-method))))
(with-test (:name (:check-keyword-args :no-error))
(assert (= (gf-with-keys-to-check 1) 3))
(assert (= (gf-with-keys-to-check 1 :b 2) 3))
(assert (equal (gf-with-keys-to-check "a") '("a" nil)))
(assert (equal (gf-with-keys-to-check "a" :b 2) '("a" 2)))
(assert (equal (gf-with-keys-to-check 'a) '(a nil)))
(assert (equal (gf-with-keys-to-check 'a :b 2) '(a nil)))
(assert (equal (gf-with-keys-to-check 'a :c 2) '(a 2)))
(assert (equal (gf-with-keys-to-check 'a :b 2 :c 3) '(a 3))))
(with-test (:name (:check-keyword-args :allow-other-keys :no-error))
(assert (= (gf-with-keys-to-check 1 :z 3 :allow-other-keys t) 3))
(assert (= (gf-with-keys-to-check 1 :b 2 :z 3 :allow-other-keys t) 3))
(assert (equal (gf-with-keys-to-check "a" :z 3 :allow-other-keys t) '("a" nil)))
(assert (equal (gf-with-keys-to-check "a" :b 2 :z 3 :allow-other-keys t) '("a" 2)))
(assert (equal (gf-with-keys-to-check 'a :z 3 :allow-other-keys t) '(a nil)))
(assert (equal (gf-with-keys-to-check 'a :b 2 :z 3 :allow-other-keys t) '(a nil)))
(assert (equal (gf-with-keys-to-check 'a :c 2 :z 3 :allow-other-keys t) '(a 2)))
(assert (equal (gf-with-keys-to-check 'a :b 2 :c 3 :allow-other-keys t) '(a 3))))
(with-test (:name (:check-keyword-args :unmatched-keyword :error))
(assert-error (gf-with-keys-to-check 1 :z 3) program-error)
(assert-error (gf-with-keys-to-check 1 :b 2 :z 3) program-error)
(assert-error (gf-with-keys-to-check "a" :z 3) program-error)
(assert-error (gf-with-keys-to-check "a" :b 2 :z 3) program-error)
(assert-error (gf-with-keys-to-check 'a :z 3) program-error)
(assert-error (gf-with-keys-to-check 'a :b 2 :z 3) program-error)
(assert-error (gf-with-keys-to-check 'a :c 2 :z 3) program-error)
(assert-error (gf-with-keys-to-check 'a :b 2 :c 3 :z 4) program-error))
(with-test (:name (:check-keyword-args :odd-keyword :error))
(assert-error (gf-with-keys-to-check 1 :b) program-error)
(assert-error (gf-with-keys-to-check 1 :b 2 :b) program-error)
(assert-error (gf-with-keys-to-check "a" :b) program-error)
(assert-error (gf-with-keys-to-check "a" :b 2 :b) program-error)
(assert-error (gf-with-keys-to-check 'a :b) program-error)
(assert-error (gf-with-keys-to-check 'a :b 2 :b) program-error)
(assert-error (gf-with-keys-to-check 'a :c 2 :b) program-error)
(assert-error (gf-with-keys-to-check 'a :b 2 :c 3 :b) program-error))
;;; class redefinition shouldn't give any warnings, in the usual case ;;; class redefinition shouldn't give any warnings, in the usual case
(defclass about-to-be-redefined () ((some-slot :accessor some-slot))) (defclass about-to-be-redefined () ((some-slot :accessor some-slot)))
(handler-bind ((warning #'error)) (handler-bind ((warning #'error))

77
tests/mop-34.impure.lisp Normal file
View file

@ -0,0 +1,77 @@
;;;; Ensuring that COMPUTE-EFFECTIVE-METHOD is usable
;;;; This software is part of the SBCL system. See the README file for
;;;; more information.
;;;;
;;;; While most of SBCL is derived from the CMU CL system, the test
;;;; files (like this one) were written from scratch after the fork
;;;; from CMU CL.
;;;;
;;;; This software is in the public domain and is provided with
;;;; absolutely no warranty. See the COPYING and CREDITS files for
;;;; more information.
(defclass foo-generic-function (standard-generic-function)
()
(:metaclass sb-mop:funcallable-standard-class))
(defclass made-method (method)
((function :initarg :function :reader sb-mop:method-function)))
(defun make-made-method (form)
(let ((fun `(lambda (args next-methods)
(declare (sb-ext:disable-package-locks call-method))
(macrolet ((call-method (m nexts)
(flet ((make-next (next)
(etypecase next
(method next)
((cons (eql make-method)) (make-made-method (cadr next))))))
`(funcall (sb-mop:method-function ,m) args ',(mapcar #'make-next nexts)))))
(declare (sb-ext:enable-package-locks call-method))
(declare (sb-ext:disable-package-locks call-next-method next-method-p))
(flet ((next-method-p () (not (null next-methods)))
(call-next-method (&rest args)
(let ((next (car next-methods)))
(if next
(funcall next args (cdr next-methods))
(error "no next method")))))
(declare (ignorable #'next-method-p #'call-next-method))
(declare (sb-ext:enable-package-locks call-next-method next-method-p))
,form)))))
(make-instance 'made-method :function (compile nil fun))))
(defmethod sb-mop:compute-discriminating-function ((gf foo-generic-function))
(let* ((apo (sb-mop:generic-function-argument-precedence-order gf))
(nreq (length apo))
(combin (sb-mop:generic-function-method-combination gf)))
(lambda (&rest args)
(let* ((methods (sb-mop:compute-applicable-methods gf (subseq args 0 nreq)))
(effective-method (sb-mop:compute-effective-method gf combin methods)))
(let ((fun (compile nil `(lambda (args)
(declare (sb-ext:disable-package-locks call-method))
(macrolet ((call-method (m nexts)
(flet ((make-next (next)
(etypecase next
(method next)
((cons (eql make-method)) (make-made-method (cadr next))))))
`(funcall (sb-mop:method-function ,m) args ',(mapcar #'make-next nexts)))))
(declare (sb-ext:enable-package-locks call-method))
,effective-method)))))
(funcall fun args))))))
(defgeneric foo (a &key b)
(:method ((a integer) &key b) (declare (ignore b)) (1+ a))
(:method ((a string) &key b) (list a b))
(:method ((a symbol) &key b c) (declare (ignore b)) (list a c))
(:method :around ((a integer) &key b) (declare (ignore b)) (1+ (call-next-method)))
(:generic-function-class foo-generic-function))
(with-test (:name (:mop-34 sb-mop:compute-effective-method :interpretable))
(assert (= (foo 1) 3))
(assert (= (foo 1 :b 2) 3))
(assert (equal (foo "a") '("a" nil)))
(assert (equal (foo "a" :b 2) '("a" 2)))
(assert (equal (foo 'a) '(a nil)))
(assert (equal (foo 'a :b 2) '(a nil)))
(assert (equal (foo 'a :c 2) '(a 2)))
(assert (equal (foo 'a :b 2 :c 3) '(a 3))))