;;;; -*- Mode: Lisp; Syntax: Common-Lisp; indent-tabs-mode: nil; Package: CLOS -*-
;;;; vim: set filetype=lisp tabstop=8 shiftwidth=2 expandtab:

;;;;
;;;;  Copyright (c) 1992, Giuseppe Attardi.
;;;;  Copyright (c) 2001, Juan Jose Garcia Ripoll.
;;;;
;;;;    ECoLisp is free software; you can redistribute it and/or
;;;;    modify it under the terms of the GNU Library General Public
;;;;    License as published by the Free Software Foundation; either
;;;;    version 2 of the License, or (at your option) any later version.
;;;;
;;;;    See file '../Copyright' for full details.

(in-package "CLOS")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; COMPILING EFFECTIVE METHODS
;;;
;;; The following functions take care of transforming the forms
;;; produced by the method combinations into effective methods. In ECL
;;; effective methods are nothing but directly callable functions.
;;; Ideally, this compilation should just produce new compiled
;;; functions. However, we do not want to cons a lot of functions, and
;;; therefore we use closures.
;;;
;;; Formerly we used to keep a list of precompiled effective methods
;;; and made a structural comparison between the current method and
;;; the precompiled ones, so as to save memory. This only causes
;;; improvements in declarative combinations. For standard combinations
;;; it should be enough with a couple of different closures and hence
;;; the structural comparison is a loss of time.
;;;
;;; This is the core routine. It produces effective methods (i.e.
;;; functions) out of the forms generated by the method combinators.
;;; We consider the following cases:
;;;  1) Ordinary methods. The function of the method is extracted.
;;;  2) Functions. They map to themselves. This only happens
;;;     when these functions have been generated by previous calls
;;;     to EFFECTIVE-METHOD-FUNCTION.
;;;  3) (CALL-METHOD method rest-methods) A closure is
;;;     generated that invokes the current method while informing
;;;     it about the remaining methods.
;;;  4) (MAKE-METHOD form) A function is created that takes the
;;;     list of arguments of the generic function and evaluates
;;;     the forms in a null environment. This is the only form
;;;     that may lead to consing of new bytecodes objects. Nested
;;;     CALL-METHOD are handled via the global macro CALL-METHOD.
;;;  5) Ordinary forms are turned into lambda forms, much like
;;;     what happens with the content of MAKE-METHOD.
;;;
(defun effective-method-function (form &optional top-level &aux first)
  (cond ((functionp form)
         form)
        ((method-p form)
         (method-function form))
        ((atom form)
         (error "Malformed effective method form:~%~A" form))
        ((eq (setf first (first form)) 'MAKE-METHOD)
         (coerce `(lambda (.combined-method-args. *next-methods*)
                    (declare (special .combined-method-args. *next-methods*))
                    ,(second form))
                 'function))
        ((eq first 'CALL-METHOD)
         (combine-method-functions
          (effective-method-function (second form))
          (mapcar #'effective-method-function (third form))))
        (top-level
         (coerce `(lambda (.combined-method-args. no-next-methods)
                    (declare (ignorable no-next-methods))
                    ,form)
                 'function))
        (t
         (error "Malformed effective method form:~%~A" form))))

;;;
;;; This function is a combinator of effective methods. It creates a
;;; closure that invokes the first method while passing the information
;;; of the remaining methods. The resulting closure (or effective method)
;;; is the equivalent of (CALL-METHOD method rest-methods)
;;;
(defun combine-method-functions (method rest-methods)
  (declare (si::c-local))
  #'(lambda (.combined-method-args. no-next-methods)
      (declare (ignorable no-next-methods))
      (funcall method .combined-method-args. rest-methods)))

(defmacro call-method (method &optional rest-methods)
  `(funcall ,(effective-method-function method)
            .combined-method-args.
            ',(and rest-methods (mapcar #'effective-method-function rest-methods))))

(defun call-next-method (&rest args)
  (declare (special .combined-method-args. *next-methods*))
  (unless *next-methods*
    (error "No next method."))
  (funcall (car *next-methods*) (or args .combined-method-args.) (rest *next-methods*)))

(defun next-method-p ()
  (declare (special *next-methods*))
  *next-methods*)

(define-compiler-macro call-next-method (&rest args)
  `(if *next-methods*
       (funcall (car *next-methods*)
                ,(if args `(list ,@args) '.combined-method-args.)
                (rest *next-methods*))
       (error "No next method.")))

(define-compiler-macro next-method-p ()
  'clos::*next-methods*)

(defun error-qualifier (m qualifier)
  (declare (si::c-local))
  (error "Standard method combination allows only one qualifier ~
          per method, either :BEFORE, :AFTER, or :AROUND; while ~
          a method with ~S was found."
         m qualifier))

(defun standard-main-effective-method (before primary after)
  (declare (si::c-local))
  #'(lambda (.combined-method-args. no-next-method)
      (declare (ignorable no-next-method))
      (dolist (i before)
        (funcall i .combined-method-args. nil))
      (if after
          (multiple-value-prog1
           (funcall (first primary) .combined-method-args. (rest primary))
           (dolist (i after)
             (funcall i .combined-method-args. nil)))
        (funcall (first primary) .combined-method-args. (rest primary)))))

(defun standard-compute-effective-method (gf methods)
  (with-early-accessors (+standard-method-slots+)
    (let* ((before ())
           (primary ())
           (after ())
           (around ()))
      (dolist (m methods)
        (let* ((qualifiers (method-qualifiers m))
               (f (method-function m)))
          (cond ((null qualifiers) (push f primary))
                ((rest qualifiers) (error-qualifier m qualifiers))
                ((eq (setq qualifiers (first qualifiers)) :BEFORE)
                 (push f before))
                ((eq qualifiers :AFTER) (push f after))
                ((eq qualifiers :AROUND) (push f around))
                (t (error-qualifier m qualifiers)))))
      ;; When there are no primary methods, an error is to be signaled,
      ;; and we need not care about :AROUND, :AFTER or :BEFORE methods.
      (when (null primary)
        (return-from standard-compute-effective-method
          #'(lambda (&rest args)
              (apply 'no-primary-method gf args))))
      ;; PRIMARY, BEFORE and AROUND are reversed because they have to
      ;; be on most-specific-first order (ANSI 7.6.6.2), while AFTER
      ;; may remain as it is because it is least-specific-order.
      (setf primary (nreverse primary)
            before (nreverse before))
      (if around
          (let ((main (if (or before after)
                          (list
                           (standard-main-effective-method before primary after))
                          primary)))
            (setf around (nreverse around))
            (combine-method-functions (first around)
                                      (nconc (rest around) main)))
          (if (or before after)
              (standard-main-effective-method before primary after)
              (combine-method-functions (first primary) (rest primary))))
      )))

;; ----------------------------------------------------------------------
;; DEFINE-METHOD-COMBINATION
;;
;; METHOD-COMBINATION objects are just a list
;;      (name arg*)
;; where NAME is the name of the method combination type defined with
;; DEFINE-METHOD-COMBINATION, and ARG* is zero or more arguments.
;;
;; For each method combination type there is an associated function,
;; and the list of all known method combination types is kept in
;; *METHOD-COMBINATIONS* in the form of property list:
;;      (mc-type-name1 function1 mc-type-name2 function2 ....)
;;
;; FUNCTIONn is the function associated to a method combination. It
;; is of type (FUNCTION (generic-function method-list) FUNCTION),
;; and it outputs an anonymous function which is the effective method.
;;

#+threads
(defparameter *method-combinations-lock* (mp:make-lock :name 'find-method-combination))
(defparameter *method-combinations* (make-hash-table :size 32 :test 'eq))

(defun search-method-combination (name)
  (mp:with-lock (*method-combinations-lock*)
    (or (gethash name *method-combinations*)
        (error "~A does not name a method combination" name))))

(defun install-method-combination (name function)
  (mp:with-lock (*method-combinations-lock*)
    (setf (gethash name *method-combinations*) function))
  name)

(defun make-method-combination (name compiler options)
  (with-early-make-instance +method-combination-slots+
    (o (find-class 'method-combination)
       :name name
       :compiler compiler
       :options options)
    o))

(defun find-method-combination (gf method-combination-type-name method-combination-options)
  (make-method-combination method-combination-type-name
                           (search-method-combination method-combination-type-name)
                           method-combination-options
                           ))

(defun define-simple-method-combination (name &key documentation
                                         identity-with-one-argument
                                         (operator name))
  `(define-method-combination
     ,name (&optional (order :MOST-SPECIFIC-FIRST))
     ((around (:AROUND))
      (principal (,name) :REQUIRED t))
     ,documentation
     (let ((main-effective-method
            `(,',operator ,@(mapcar #'(lambda (x) `(CALL-METHOD ,x NIL))
                                    (if (eql order :MOST-SPECIFIC-LAST)
                                        (reverse principal)
                                        principal)))))
       (cond (around
              `(call-method ,(first around)
                (,@(rest around) (make-method ,main-effective-method))))
             (,(if identity-with-one-argument
                   '(rest principal)
                   t)
              main-effective-method)
             (t (second main-effective-method))))))

(defun define-complex-method-combination (form)
  (declare (si::c-local))
  (flet ((syntax-error ()
           (error "~S is not a valid DEFINE-METHOD-COMBINATION form"
                  form)))
    (destructuring-bind (name lambda-list method-groups &rest body &aux
                         (group-names '())
                         (group-checks '())
                         (group-after '())
                         (generic-function '.generic-function.)
                         (method-arguments '())
                         decls documentation)
        form
      (unless (symbolp name) (syntax-error))
      (let ((x (first body)))
        (when (and (consp x) (eql (first x) :ARGUMENTS))
          (error "Option :ARGUMENTS is not supported in DEFINE-METHOD-COMBINATION.")))
      (let ((x (first body)))
        (when (and (consp x) (eql (first x) :GENERIC-FUNCTION))
          (setf body (rest body))
          (unless (symbolp (setf generic-function (second x)))
            (syntax-error))))
      (multiple-value-setq (decls body documentation)
        (si::find-declarations body t))
      (dolist (group method-groups)
        (destructuring-bind (group-name predicate &key description
                                  (order :most-specific-first) (required nil))
            group
          (if (symbolp group-name)
              (push group-name group-names)
              (syntax-error))
          (let ((condition
                (cond ((eql predicate '*) 'T)
                      ((and predicate (symbolp predicate))
                       `(,predicate .METHOD-QUALIFIERS.))
                      ((and (listp predicate)
                            (let* ((q (last predicate 0))
                                   (p (copy-list (butlast predicate 0))))
                              (when (every #'symbolp p)
                                (if (eql q '*)
                                    `(every #'equal ',p .METHOD-QUALIFIERS.)
                                    `(equal ',p .METHOD-QUALIFIERS.))))))
                      (t (syntax-error)))))
            (push `(,condition (push .METHOD. ,group-name)) group-checks))
          (when required
            (push `(unless ,group-name
                    (error "Method combination: ~S. No methods ~
                            in required group ~S." ',name ,group-name))
                  group-after))
          (case order
            (:most-specific-first
             (push `(setf ,group-name (nreverse ,group-name)) group-after))
            (:most-specific-last)
            (otherwise
             (let ((order-var (gensym)))
               (setf group-names (append group-names (list (list order-var order)))
                     group-after (list* `(when (eq ,order-var :most-specific-first)
                                           (setf ,group-name (nreverse ,group-name)))
                                        group-after)))))))
      `(progn
         ,@(si::expand-set-documentation name 'method-combination documentation)
         (install-method-combination ',name
            (lambda (,generic-function .methods-list. ,@lambda-list)
              (declare (ignorable ,generic-function))
              ,@decls
              (block ,name
                (let (,@group-names)
                  (dolist (.method. .methods-list.)
                    (let ((.method-qualifiers. (method-qualifiers .method.)))
                      (cond ,@(nreverse group-checks)
                            (t (invalid-method-error .method.
                                 "Method qualifiers ~S are not allowed in the method~
                                  combination ~S." .method-qualifiers. ',name)))))
                  ,@group-after
                  (effective-method-function (progn ,@body) t))))))
      )))

(defmacro define-method-combination (name &body body)
  (if (and body (listp (first body)))
      (define-complex-method-combination (list* name body))
      (apply #'define-simple-method-combination name body)))

(defun method-combination-error (format-control &rest args)
  ;; FIXME! We should emit a more detailed error!
  (error "Method-combination error:~%~S"
         (apply #'format nil format-control args)))

(defun invalid-method-error (method format-control &rest args)
  (error "Invalid method error for ~A~%~S"
         method
         (apply #'format nil format-control args)))

;;; ----------------------------------------------------------------------
;;; COMPUTE-EFFECTIVE-METHOD
;;;

(eval-when (compile)
  (let* ((class (find-class 'method-combination)))
    (define-compiler-macro method-combination-compiler (o)
      `(si::instance-ref ,o ,(slot-definition-location (gethash 'compiler (slot-table class)))))
    (define-compiler-macro method-combination-options (o)
      `(si::instance-ref ,o ,(slot-definition-location (gethash 'options (slot-table class)))))))

(defun std-compute-effective-method (gf method-combination applicable-methods)
  (declare (type method-combination method-combination)
           (type generic-function gf)
           (optimize speed (safety 0)))
  (with-early-accessors (+method-combination-slots+)
    (let* ((compiler (method-combination-compiler method-combination))
           (options (method-combination-options method-combination)))
      (if options
          (apply compiler gf applicable-methods options)
          (funcall compiler gf applicable-methods)))))

(defun compute-effective-method-function (gf method-combination applicable-methods)
  ;; Cannot be inlined because it will be a method
  (declare (notinline compute-effective-method))
  (let ((form (compute-effective-method gf method-combination applicable-methods)))
    (let ((aux form) f)
      (if (and (listp aux)
                 (eq (pop aux) 'funcall)
                 (functionp (setf f (pop aux)))
                 (eq (pop aux) '.combined-method-args.)
                 (eq (pop aux) '*next-methods*))
          f
          (effective-method-function form t)))))

(defun compute-effective-method (gf method-combination applicable-methods)
  `(funcall ,(std-compute-effective-method gf method-combination applicable-methods)
            .combined-method-args. *next-methods*))

(install-method-combination 'standard 'standard-compute-effective-method)
(progn
  (define-method-combination progn :identity-with-one-argument t)
  (define-method-combination and :identity-with-one-argument t)
  (define-method-combination max :identity-with-one-argument t)
  (define-method-combination + :identity-with-one-argument t)
  (define-method-combination nconc :identity-with-one-argument t)
  (define-method-combination append :identity-with-one-argument nil)
  (define-method-combination list :identity-with-one-argument nil)
  (define-method-combination min :identity-with-one-argument t)
  (define-method-combination or :identity-with-one-argument t))

