;; Copyright (C) 1994 M. Hagiya, W. Schelter, T. Yuasa

;; This file is part of GNU Common Lisp, herein referred to as GCL
;;
;; GCL 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, or (at your option)
;; any later version.
;; 
;; GCL is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public 
;; License for more details.
;; 
;; You should have received a copy of the GNU Library General Public License 
;; along with GCL; see the file COPYING.  If not, write to the Free Software
;; Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.


;;;;        trace.lsp 
;;;;
;;;;        Tracer package for Common Lisp

;;;;;; Modified by Matt Kaufmann to allow tracing options.


;; If you are working in another package you should (import 'si::arglist)
;; to avoid typing the si::

(in-package 'lisp)

(export '(trace untrace))
(export 'step)


(in-package 'system)

;;(proclaim '(optimize (safety 2) (space 3)))


(defvar *trace-level* 0)
(defvar *trace-list* nil)


(defmacro trace (&rest r)
  (if (null r)
      '(mapcar #'car *trace-list*)
    `(let ((old (copy-list *trace-list*)) finish-flg)
       (unwind-protect
	   (prog1 (mapcan #'trace-one ',r)
	     (setq finish-flg t))
	 (when (null finish-flg)
	       (format *standard-output* "~%Newly traced functions:  ~S"
		       (mapcar #'car (set-difference *trace-list* old :test #'equal))))))))

(defmacro untrace (&rest r)
  (if (null r)
      '(mapcan #'untrace-one (mapcar #'car *trace-list*))
      `(mapcan #'untrace-one ',r)))

(defun trace-one-preprocess (x)
  (cond
   ((symbolp x)
    (trace-one-preprocess (list x)))
   (t					; We've checked for CONSP with null last CDR
    (do ((tail (cdr x) (cddr tail))
	 (declarations)
	 (entryform `(cons (quote ,(car x)) arglist))
	 (exitform `(cons (quote ,(car x)) values))
	 (condform t)
	 (entrycondform t)
	 (exitcondform t)
	 (depth) (depthvar))
	((null tail)
	 (when depth
	       ;; Modify the :cond so that it first checks depth, and then
	       ;; modify the :entry so that it first increments depth.  Notice
	       ;; that :cond will be fully evaluated before depth is incremented.
	       (setq depthvar (gensym))
	       ;; now reset the condform
	       (if
		(eq condform t)
		(setq condform
		      `(< ,depthvar ,depth))
		 (setq condform `(if (< ,depthvar ,depth) ,condform nil)))
	       (setq declarations (cons (cons depthvar 0) declarations))
	       ;; I'll have the depth be incremented for all the entry stuff and no exit stuff,
	       ;; since I don't see any more uniform, logical way to do this.
	       (setq entrycondform
		     `(progn
			(setq ,depthvar (1+ ,depthvar))
			,entrycondform))
	       (setq exitcondform
		     `(progn
			  (setq ,depthvar (1- ,depthvar))
			,exitcondform)))
	 `(,(car x) ,declarations
	   (quote ,condform)
	   (quote ,entrycondform) (quote ,entryform)
	   (quote ,exitcondform) (quote ,exitform)))
	(case (car tail)
	      (:declarations
	       (setq declarations
		     (do ((decls (cadr tail) (cdr decls))
			  (result))
			 ((null decls) result)
			 (setq result
			       (cons (if (symbolp (car decls))
					 (cons (car decls) nil)
				       (cons (caar decls) (eval (cadar decls))))
				     result)))))
	      (:cond (setq condform (cadr tail)))
	      (:entrycond (setq entrycondform (cadr tail)))
	      (:entry (setq entryform (cadr tail)))
	      (:exitcond (setq exitcondform (cadr tail)))
	      (:exit (setq exitform (cadr tail))) 
	      (:depth (setq depth (cadr tail)))
	      (otherwise nil))))))

(defun check-trace-spec (form)
  (or (symbolp form)
      (if (and (consp form) (null (cdr (last form))))
	  (check-trace-args form (cdr form) nil)
	(error "Each trace spec must be a symbol or a list terminating in NIL, but ~S is not~&."
	       form))))

(defun check-declarations (declarations &aux decl)
  (when (consp declarations)
	(setq decl (if (consp (car declarations)) (car declarations) (list (car declarations) nil)))
	(when (not (symbolp (car decl)))
	      (error "Declarations are supposed to be of symbols, but ~S is not a symbol.~&"
		     (car decl)))
	(when (cddr decl)
	      (error "Expected a CDDR of NIL in ~S.~&"
		     decl))
	(when (assoc (car decl) (all-trace-declarations))
	      (error "The variable ~A is already declared for tracing"
		     (car decl)))))

(defun check-trace-args (form args acc-keywords)
  (when args
	(cond
	 ((null (cdr args))
	  (error "A trace spec must have odd length, but ~S does not.~&"
		 form))
	 ((member (car args) acc-keywords)
	  (error "The keyword ~A occurred twice in the spec ~S~&"
		 (car args) form))
	 (t
	  (case (car args)
		((:entry :exit :cond :entrycond :exitcond)
		 (check-trace-args form (cddr args) (cons (car args) acc-keywords)))
		(:depth
		 (when (not (and (integerp (cadr args))
				 (> (cadr args) 0)))
		       (error
			"~&Specified depth should be a positive integer, but~&~S is not.~&"
			(cadr args)))
		 (check-trace-args form (cddr args) (cons :depth acc-keywords)))
		(:declarations
		 (check-declarations (cadr args))
		 (check-trace-args form (cddr args) (cons :declarations acc-keywords)))
		(otherwise
		 (error "Expected :entry, :exit, :cond, :depth, or :declarations~&~
                         in ~S where instead there was ~S~&"
			form (car args))))))))

(defun trace-one (form &aux f (fname (if (consp form) (car form) form)))
  (when (null (fboundp fname))
        (format *trace-output* "The function ~S is not defined.~%" fname)
        (return-from trace-one nil))
  (when (special-form-p fname)
        (format *trace-output* "~S is a special form.~%" fname)
        (return-from trace-one nil))
  (when (macro-function fname)
        (format *trace-output* "~S is a macro.~%" fname)
        (return-from trace-one nil))
  (when (get fname 'traced)
	(untrace-one fname))
  (check-trace-spec form)
  (setq form (trace-one-preprocess form))
  (si:fset (setq f (gensym)) (symbol-function fname))
  (eval `(defun ,fname (&rest args)
	   (trace-call ',f args
		       ,@(cddr form))))
  (si:putprop fname f 'traced)
  (setq *trace-list* (cons (cons fname (cadr form)) *trace-list*))
  (list fname))

(defun reset-trace-declarations (declarations)
  (when declarations
	(set (caar declarations) (cdar declarations))
	(reset-trace-declarations (cdr declarations))))

(defun all-trace-declarations ( &aux result)
  (dolist (v *trace-list*)
	  (setq result (append result (cdr v))))
  result)
	  
(defun trace-call (temp-name args cond entrycond entry exitcond exit
			 &aux (*trace-level* *trace-level*) vals indent)
  (when (= *trace-level* 0)
	(reset-trace-declarations (all-trace-declarations)))
  (cond
   ((eval `(let ((arglist (quote ,args))) ,cond))
    (setq *trace-level* (1+ *trace-level*))
    (setq indent (min (* *trace-level* 2) 20))
    (fresh-line *trace-output*)
    (when (or (eq entrycond t)		;optimization for common value
	      (eval `(let ((arglist (quote ,args))) ,entrycond)))
	  ;; put out the prompt before evaluating
	  (format *trace-output*
		  "~V@T~D> "
		  indent *trace-level*)
	  (format *trace-output*
		  "~S~%"
		  (eval `(let ((arglist (quote ,args))) ,entry)))
	  (fresh-line *trace-output*))
    (setq vals (multiple-value-list (apply temp-name args)))
    (when (or (eq exitcond t)		;optimization for common value
	      (eval `(let ((arglist (quote ,args)) (values (quote ,vals)))
		       ,exitcond)))
	  ;; put out the prompt before evaluating
	  (format *trace-output*
		  "~V@T<~D "
		  indent
		  *trace-level*) 
	  (format *trace-output*
		  "~S~%"
		  (eval `(let ((arglist (quote ,args)) (values (quote ,vals))) ,exit))))
    (setq *trace-level* (1- *trace-level*))
    (values-list vals))
   (t (apply temp-name args))))

(defun untrace-one (fname &aux sym)
  (cond ((setq sym (get fname 'traced))
	 (remprop fname 'traced)
	 (cond
	  ((not (fboundp fname))
	   (format *trace-output*
		   "The function ~S was traced, but is no longer defined.~%"
		   fname))

	  ;;(LAMBDA-BLOCK block-name lambda-list (TRACE-CALL ... ))
	  ((and (consp (symbol-function fname))
		(consp (nth 3 (symbol-function fname)))
		(eq (car (nth 3 (symbol-function fname))) 'trace-call))
	   (si:fset fname (symbol-function sym)))
	  (t
	   (format *trace-output*
		   "The function ~S was traced, but redefined.~%"
		   fname)))
	 (setq *trace-list*
	       (delete-if #'(lambda (u) (eq (car u) fname))
			  *trace-list* :count 1))
	 (list fname))
        (t
         (format *trace-output* "The function ~S is not traced.~%" fname)
         nil)))

#| Example of tracing a function "fact" so that only the outermost call is traced.

(defun fact (n) (if (= n 0) 1 (* n (fact (1- n)))))

;(defvar in-fact nil)
(trace (fact :declarations ((in-fact nil))
	     :cond
	     (null in-fact)
	     :entry
	     (progn
	       (setq in-fact t)
	       (princ "Here comes input ")
	       (cons 'fact arglist))
             :exit
             (progn (setq in-fact nil)
		    (princ "Here comes output ")
                    (cons 'fact values))))

; Example of tracing fact so that only three levels are traced

(trace (fact :declarations
	     ((fact-depth 0))
	     :cond
	     (and (< fact-depth 3)
		  (setq fact-depth (1+ fact-depth)))
	     :exit
	     (progn (setq fact-depth (1- fact-depth)) (cons 'fact values))))
|#



(defvar *step-level* 0)
(defvar *step-quit* nil)
(defvar *step-function* nil)

(defvar *old-print-level* nil)
(defvar *old-print-length* nil)


(defun step-read-line ()
  (do ((char (read-char *debug-io*) (read-char *debug-io*)))
      ((or (char= char #\Newline) (char= char #\Return)))))

(defmacro if-error (error-form form)
  (let ((v (gensym)) (f (gensym)) (b (gensym)))
    `(let (,v ,f)
       (block ,b
         (unwind-protect (setq ,v ,form ,f t)
           (return-from ,b (if ,f ,v ,error-form)))))))

(defmacro step (form)
  `(let* ((*old-print-level* *print-level*)
          (*old-print-length* *print-length*)
          (*print-level* 2)
          (*print-length* 2))
     (read-line)
     (format *debug-io* "Type ? and a newline for help.~%")
     (setq *step-quit* nil)
     (stepper ',form nil)))

(defun stepper (form &optional env
                &aux values (*step-level* *step-level*) indent)
  (when (eq *step-quit* t)
    (return-from stepper (evalhook form nil nil env)))
  (when (numberp *step-quit*)
    (if (>= (1+ *step-level*) *step-quit*)
        (return-from stepper (evalhook form nil nil env))
        (setq *step-quit* nil)))
  (when *step-function*
    (if (and (consp form) (eq (car form) *step-function*))
        (let ((*step-function* nil))
          (return-from stepper (stepper form env)))
        (return-from stepper (evalhook form #'stepper nil env))))
  (setq *step-level* (1+ *step-level*))
  (setq indent (min (* *step-level* 2) 20))
  (loop
    (format *debug-io* "~VT~S " indent form)
    (finish-output *debug-io*)
    (case (do ((char (read-char *debug-io*) (read-char *debug-io*)))
              ((and (char/= char #\Space) (char/= char #\Tab)) char))
          ((#\Newline #\Return)
           (setq values
                 (multiple-value-list
                  (evalhook form #'stepper nil env)))
           (return))
          ((#\n #\N)
           (step-read-line)
           (setq values
                 (multiple-value-list
                  (evalhook form #'stepper nil env)))
           (return))
          ((#\s #\S)
           (step-read-line)
           (setq values
                 (multiple-value-list
                  (evalhook form nil nil env)))
           (return))
          ((#\p #\P)
           (step-read-line)
           (write form
                  :stream *debug-io*
                  :pretty t :level nil :length nil)
           (terpri))
          ((#\f #\F)
           (let ((*step-function*
                  (if-error nil
                            (prog1 (read-preserving-whitespace *debug-io*)
                                   (step-read-line)))))
             (setq values
                   (multiple-value-list
                    (evalhook form #'stepper nil env)))
             (return)))
          ((#\q #\Q)
           (step-read-line)
           (setq *step-quit* t)
           (setq values
                 (multiple-value-list
                  (evalhook form nil nil env)))
           (return))
          ((#\u #\U)
           (step-read-line)
           (setq *step-quit* *step-level*)
           (setq values
                 (multiple-value-list
                  (evalhook form nil nil env)))
           (return))
          ((#\e #\E)
           (let ((env1 env))
             (dolist (x
                      (if-error nil
                                (multiple-value-list
                                 (evalhook
                                  (if-error nil
                                            (prog1
                                             (read-preserving-whitespace
                                              *debug-io*)
                                             (step-read-line)))
                                  nil nil env1))))
                     (write x
                            :stream *debug-io*
                            :level *old-print-level*
                            :length *old-print-length*)
                     (terpri *debug-io*))))
          ((#\r #\R)
           (let ((env1 env))
             (setq values
                   (if-error nil
                             (multiple-value-list
                              (evalhook
                               (if-error nil
                                         (prog1
                                          (read-preserving-whitespace
                                           *debug-io*)
                                          (step-read-line)))
                               nil nil env1)))))
           (return))
          ((#\b #\B)
           (step-read-line)
           (let ((*ihs-base* (1+ *ihs-top*))
                 (*ihs-top* (1- (ihs-top)))
                 (*current-ihs* *ihs-top*))
             (simple-backtrace)))
          (t
           (step-read-line)
           (terpri)
           (format *debug-io*
                  "Stepper commands:~%~
		n (or N or Newline):	advances to the next form.~%~
		s (or S):		skips the form.~%~
		p (or P):		pretty-prints the form.~%~
                f (or F) FUNCTION:	skips until the FUNCTION is called.~%~
                q (or Q):		quits.~%~
                u (or U):		goes up to the enclosing form.~%~
                e (or E) FORM:		evaluates the FORM ~
					and prints the value(s).~%~
                r (or R) FORM:		evaluates the FORM ~
					and returns the value(s).~%~
                b (or B):		prints backtrace.~%~
		?:			prints this.~%")
           (terpri))))
  (when (or (constantp form) (and (consp form) (eq (car form) 'quote)))
        (return-from stepper (car values)))
  (if (endp values)
      (format *debug-io* "~V@T=~%" indent)
      (do ((l values (cdr l))
           (b t nil))
          ((endp l))
        (if b
            (format *debug-io* "~V@T= ~S~%" indent (car l))
            (format *debug-io* "~V@T& ~S~%" indent (car l)))))
  (setq *step-level* (- *step-level* 1))
  (values-list values))

