;;
;;  SELECT-MATCH macro (and IN macro)
;;
;; Copyright 1990   Stephen Adams
;;
;; You are free to copy, distribute and make derivative works of this
;; source provided that this copyright notice is displayed near the
;; beginning of the file.  No liability is accepted for the
;; correctness or performance of the code.  If you modify the code
;; please indicate this fact both at the place of modification and in
;; this copyright message.
;;
;;   Stephen Adams
;;   Department of Electronics and Computer Science
;;   University of Southampton
;;   SO9 5NH, UK
;;
;; sra@ecs.soton.ac.uk
;;

;;
;;  Synopsis:
;;
;;  (select-match expression
;;      (pattern  action+)*)
;;
;;      --- or ---
;;
;;  (select-match expression
;;      pattern => expression
;;      pattern => expression
;;      ...)
;;
;;  pattern ->  constant		;egs  1, #\x, #c(1.0 1.1)
;;          |   symbol                  ;matches anything
;;          |   'anything               ;must be EQUAL
;;          |   (pattern = pattern)     ;both patterns must match
;;          |   (#'function pattern)    ;predicate test
;;          |   (pattern . pattern)	;cons cell
;;

;;  Example
;;
;;  (select-match item
;;      (('if e1 e2 e3) 'if-then-else)				;(1)
;;      ((#'oddp k)     'an-odd-integer)			;(2)
;;      (((#'treep tree) = (hd . tl))   'something-else)	;(3)
;;      (other          'anything-else))			;(4)
;;
;;  Notes
;;
;;  .   Each pattern is tested in turn.  The first match is taken.
;;
;;  .   If no pattern matches, an error is signalled.
;;
;;  .   Constant patterns (things X for which (CONSTANTP X) is true, i.e.
;;      numbers, strings, characters, etc.) match things which are EQUAL.
;;
;;  .   Quoted patterns (which are CONSTANTP) are constants.
;;
;;  .   Symbols match anything. The symbol is bound to the matched item
;;      for the execution of the actions.
;;      For example, (SELECT-MATCH '(1 2 3)
;;                      (1 . X) => X)
;;      returns (2 3) because X is bound to the cdr of the candidate.
;;
;;  .   The two pattern match (p1 = p2) can be used to name parts
;;      of the matched structure.  For example, (ALL = (HD . TL))
;;      matches a cons cell. ALL is bound to the cons cell, HD to its car
;;      and TL to its tail.
;;
;;  .   A predicate test applies the predicate to the item being matched.
;;      If the predicate returns NIL then the match fails.
;;      If it returns truth, then the nested pattern is matched.  This is
;;      often just a symbol like K in the example.
;;
;;  .   Care should be taken with the domain values for predicate matches.
;;      If, in the above eg, item is not an integer, an error would occur
;;      during the test.  A safer pattern would be
;;          (#'integerp (#'oddp k))
;;      This would only test for oddness of the item was an integer.
;;
;;  .   A single symbol will match anything so it can be used as a default
;;      case, like OTHER above.
;;

(defpackage :slynk-match
  (:use :cl)
  (:export #:match))

(in-package :slynk-match)

(defmacro match (expression &body patterns)
  `(select-match ,expression ,@patterns))

(defmacro select-match (expression &rest patterns)
  (let* ((do-let (not (atom expression)))
         (key    (if do-let (gensym) expression))
         (cbody  (expand-select-patterns key patterns))
         (cform  `(cond . ,cbody)))
    (if do-let
        `(let ((,key ,expression)) ,cform)
        cform)))

(defun expand-select-patterns (key patterns)
  (if (eq (second patterns) '=>)
      (expand-select-patterns-style-2 key patterns)
      (expand-select-patterns-style-1 key patterns)))

(defun expand-select-patterns-style-1 (key patterns)
  (if (null patterns)
      `((t (error "Case select pattern match failure on ~S" ,key)))
      (let* ((pattern  (caar patterns))
             (actions  (cdar patterns))
             (rest     (cdr patterns))
             (test     (compile-select-test key pattern))
             (bindings (compile-select-bindings key pattern actions)))
        `(,(if bindings `(,test (let ,bindings . ,actions))
               `(,test . ,actions))
           . ,(unless (eq test t)
                (expand-select-patterns-style-1 key rest))))))

(defun expand-select-patterns-style-2 (key patterns)
  (cond ((null patterns)
         `((t (error "Case select pattern match failure on ~S" ,key))))
        (t (when (or (< (length patterns) 3)
                     (not (eq (second patterns) '=>)))
             (error "Illegal patterns: ~S" patterns))
           (let* ((pattern  (first patterns))
                  (actions  (list (third patterns)))
                  (rest     (cdddr patterns))
                  (test     (compile-select-test key pattern))
                  (bindings (compile-select-bindings key pattern actions)))
             `(,(if bindings `(,test (let ,bindings . ,actions))
                    `(,test . ,actions))
                . ,(unless (eq test t)
                     (expand-select-patterns-style-2 key rest)))))))

(defun compile-select-test (key pattern)
  (let ((tests (remove t (compile-select-tests key pattern))))
    (cond
      ;; note AND does this anyway, but this allows us to tell if
      ;; the pattern will always match.
      ((null tests)         t)
      ((= (length tests) 1) (car tests))
      (t                    `(and . ,tests)))))

(defun compile-select-tests (key pattern)
  (cond ((constantp pattern)   `((,(cond ((numberp pattern) 'eql)
                                         ((symbolp pattern) 'eq)
                                         (t                'equal))
                                   ,key ,pattern)))
        ((symbolp pattern)      '(t))
        ((select-double-match? pattern)
         (append
          (compile-select-tests key (first pattern))
          (compile-select-tests key (third pattern))))
        ((select-predicate? pattern)
         (append
          `((,(second (first pattern)) ,key))
          (compile-select-tests key (second pattern))))
        ((consp pattern)
         (append
          `((consp ,key))
          (compile-select-tests (cs-car key) (car
                                               pattern))
          (compile-select-tests (cs-cdr key) (cdr
                                               pattern))))
        (t (error "Illegal select pattern: ~S" pattern))))


(defun compile-select-bindings (key pattern action)
  (cond ((constantp pattern) '())
        ((symbolp pattern)
         (if (select-in-tree pattern action)
             `((,pattern ,key))
             '()))
        ((select-double-match? pattern)
         (append
          (compile-select-bindings key (first pattern) action)
          (compile-select-bindings key (third pattern) action)))
        ((select-predicate? pattern)
         (compile-select-bindings key (second pattern) action))
        ((consp pattern)
         (append
          (compile-select-bindings (cs-car key) (car pattern)
                                   action)
          (compile-select-bindings (cs-cdr key) (cdr pattern)
                                   action)))))

(defun select-in-tree (atom tree)
  (or (eq atom tree)
      (if (consp tree)
          (or (select-in-tree atom (car tree))
              (select-in-tree atom (cdr tree))))))

(defun select-double-match? (pattern)
  ;;  (<pattern> = <pattern>)
  (and (consp pattern) (consp (cdr pattern)) (consp (cddr pattern))
       (null (cdddr pattern))
       (eq (second pattern) '=)))

(defun select-predicate? (pattern)
  ;; ((function <f>) <pattern>)
  (and (consp pattern)
       (consp (cdr pattern))
       (null (cddr pattern))
       (consp (first pattern))
       (consp (cdr (first pattern)))
       (null (cddr (first pattern)))
       (eq (caar pattern) 'function)))

(defun cs-car (exp)
  (cs-car/cdr 'car exp
               '((car . caar)     (cdr . cadr)    (caar . caaar) (cadr . caadr)
                 (cdar . cadar)   (cddr . caddr)
                 (caaar . caaaar) (caadr . caaadr) (cadar . caadar)
                 (caddr . caaddr) (cdaar . cadaar) (cdadr . cadadr)
                 (cddar . caddar) (cdddr . cadddr))))

(defun cs-cdr (exp)
  (cs-car/cdr 'cdr exp
               '((car . cdar)    (cdr . cddr)    (caar . cdaar)  (cadr . cdadr)
                 (cdar . cddar)  (cddr . cdddr)
                 (caaar . cdaaar)    (caadr . cdaadr)    (cadar . cdadar)
                 (caddr . cdaddr)    (cdaar . cddaar)    (cdadr . cddadr)
                 (cddar . cdddar)    (cdddr . cddddr))))

(defun cs-car/cdr (op exp table)
  (if (and (consp exp) (= (length exp) 2))
      (let ((replacement  (assoc (car exp) table)))
        (if replacement
            `(,(cdr replacement) ,(second exp))
            `(,op ,exp)))
      `(,op ,exp)))

;; (setf c1 '(select-match x (a 1) (b 2 3 4)))
;; (setf c2 '(select-match (car y)
;;             (1 (print 100) 101) (2 200) ("hello" 5) (:x 20) (else (1+
;;  else))))
;; (setf c3 '(select-match (caddr y)
;;             ((all = (x y)) (list x y all))
;;             ((a '= b)      (list 'assign a b))
;;             ((#'oddp k)     (1+ k)))))