multimethod.ss
#lang scheme
(require
 scheme/stxparam
 srfi/26
 "overloads.ss")

(provide
 (struct-out exn:fail:multimethod))

(define-struct multimethod
  (make-signature
   current-overloads)
  #:property prop:procedure
  (make-keyword-procedure
   (λ (kw-args kw-vals mm . args)
     (local [(define overloads
               ((multimethod-current-overloads mm)))
             (define signature
               (keyword-apply (multimethod-make-signature mm)
                 kw-args kw-vals args))
             (define methods
               (find-methods overloads signature))
             (define ((make-method-call methods))
               (if (null? methods)
                   (raise (make-exn:fail:multimethod
                           (format
                            "multimethod: no unique next method for signature ~e"
                            signature)
                           (current-continuation-marks)
                           overloads signature))
                   (keyword-apply (car methods)
                     kw-args kw-vals (make-method-call (cdr methods)) args)))]
       ((make-method-call methods))))))

(define (make-multimethod* make-signature
                           #:overloads [overloads (make-overloads)])
  (make-multimethod make-signature (make-parameter overloads)))

(provide/contract
 [multimethod? (any/c . -> . boolean?)]
 [multimethod-make-signature (multimethod? . -> . procedure?)]
 [multimethod-current-overloads (multimethod? . -> . (parameter/c overloads?))]
 [rename make-multimethod* make-multimethod
         ((procedure?) (#:overloads overloads?) . ->* . multimethod?)])

(define-syntax define-multimethod
  (syntax-rules (::)
    [(define-multimethod (name . args) :: signature options ...)
     (define-multimethod name :: (λ args signature)
       options ...)]
    [(define-multimethod name :: signature options ...)
     (define name
       (make-multimethod* signature options ...))]))

(define-syntax parameterize-multimethod
  (syntax-rules ()
    [(parameterize-multimethod (name ...)
       expr ...)
     (parameterize ([(multimethod-current-overloads name)
                     ((multimethod-current-overloads name))]
                    ...)
       expr ...)]))

(define-syntax-parameter call-next-method
  (λ (stx)
    (raise-syntax-error #f "used out of method context" stx)))

(define-syntax define-method
  (syntax-rules (::)
    [(define-method (name . args) #:default
       expr ...)
     (define-method name #:default
       (λ (local-next-method . args)
         (syntax-parameterize ([call-next-method (make-rename-transformer
                                                  #'local-next-method)])
           expr ...)))]
    [(define-method name #:default method)
     (let* ([current-overloads (multimethod-current-overloads name)]
            [overloads (current-overloads)]
            [signature (overloads-default-signature overloads)])
       (current-overloads
        (cond
          [(begin method)
           => (cut set-method overloads signature <>)]
          [else
           (remove-method overloads signature)])))]
    [(define-method (name . args) :: signature
       expr ...)
     (define-method name :: `signature
       (λ (local-next-method . args)
         (syntax-parameterize ([call-next-method (make-rename-transformer
                                                  #'local-next-method)])
           expr ...)))]
    [(define-method name :: signature method)
     (let* ([current-overloads (multimethod-current-overloads name)]
            [overloads (current-overloads)])
       (current-overloads
        (cond
          [(begin method)
           => (cut set-method overloads signature <>)]
          [else
           (remove-method overloads signature)])))]))

(define-syntax define-preference
  (syntax-rules (:: < > =)
    [(define-preference name :: (< signature-a signature-b))
     (let ([current-overloads (multimethod-current-overloads name)])
       (current-overloads
        (prefer-method (current-overloads) `signature-a `signature-b)))]
    [(define-preference name :: (> signature-a signature-b))
     (let ([current-overloads (multimethod-current-overloads name)])
       (current-overloads
        (prefer-method (current-overloads) `signature-b `signature-a)))]
    [(define-preference name :: (= signature-a signature-b))
     (let ([current-overloads (multimethod-current-overloads name)])
       (current-overloads
        ((let ([signature-a `signature-a]
               [signature-b `signature-b])
           (compose
            (cut unprefer-method <> signature-b signature-a)
            (cut unprefer-method <> signature-a signature-b)))
         (current-overloads))))]))

(provide
 define-multimethod
 parameterize-multimethod
 call-next-method
 define-method
 define-preference)