;;;; (ice-9 debugger trap-hooks) -- abstraction of libguile's traps interface

;;; Copyright (C) 2002 Free Software Foundation, Inc.
;;;
;; This library is free software; you can redistribute it and/or
;; modify it under the terms of the GNU Lesser General Public
;; License as published by the Free Software Foundation; either
;; version 2.1 of the License, or (at your option) any later version.
;; 
;; This library 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
;; Lesser General Public License for more details.
;; 
;; You should have received a copy of the GNU Lesser General Public
;; License along with this library; if not, write to the Free Software
;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

;;; This module provides an abstraction around Guile's low level trap
;;; handler interface; its aim is to make the low level trap mechanism
;;; shareable between the debugger and other applications, and to
;;; insulate the rest of the debugger code a bit from changes that may
;;; occur in the low level trap interface in future.

(define-module (ice-9 debugger trap-hooks)
  #:use-module (ice-9 debugger trc)
  #:export (add-trapped-stack-id!
	    remove-trapped-stack-id!
	    before-apply-frame-hook
	    before-enter-frame-hook
	    before-exit-frame-hook
	    after-apply-frame-hook
	    after-enter-frame-hook
	    after-exit-frame-hook
	    add-apply-frame-hook!
	    add-breakpoint-hook!
	    add-enter-frame-hook!
	    add-exit-frame-hook!
	    add-trace-hook!
	    remove-apply-frame-hook!
	    remove-breakpoint-hook!
	    remove-enter-frame-hook!
	    remove-exit-frame-hook!
	    remove-trace-hook!
	    debug-hook-membership))

;;; The current low level traps interface is as follows.
;;;
;;; All trap handlers are subject to SCM_TRAPS_P, which is controlled
;;; by the `traps' setting of `(evaluator-traps-interface)' but also
;;; (and more relevant in most cases) by the `with-traps' procedure.
;;; Basically, `with-traps' sets SCM_TRAPS_P to 1 during execution of
;;; its thunk parameter.
;;;
;;; Note that all trap handlers are called with SCM_TRAPS_P set to 0
;;; for the duration of the call, to avoid nasty recursive trapping
;;; loops.  If a trap handler knows what it is doing, it can override
;;; this by `(trap-enable traps)'.
;;;
;;; The apply-frame handler is called when Guile is about to perform
;;; an application if EITHER the `apply-frame' evaluator trap option
;;; is set, OR the `trace' debug option is set and the procedure to
;;; apply has its `trace' procedure property set.  The arguments
;;; passed are:
;;;
;;; - the symbol 'apply-frame
;;;
;;; - a continuation or debug object describing the current stack
;;;
;;; - a boolean indicating whether the application is tail-recursive.
;;;
;;; The enter-frame handler is called when the evaluator begins a new
;;; evaluation frame if EITHER the `enter-frame' evaluator trap option
;;; is set, OR the `breakpoints' debug option is set and the code to
;;; be evaluated has its `breakpoint' source property set.  The
;;; arguments passed are:
;;;
;;; - the symbol 'enter-frame
;;;
;;; - a continuation or debug object describing the current stack
;;;
;;; - a boolean indicating whether the application is tail-recursive.
;;;
;;; - an unmemoized copy of the expression to be evaluated.
;;;
;;; If the `enter-frame' evaluator trap option is set, the enter-frame
;;; handler is also called when about to perform an application in
;;; SCM_APPLY, immediately before possible calling the apply-frame
;;; handler.  (I don't totally understand this.)  In this case, the
;;; arguments passed are:
;;;
;;; - the symbol 'enter-frame
;;;
;;; - a continuation or debug object describing the current stack.
;;;
;;; The exit-frame handler is called when Guile exits an evaluation
;;; frame (in SCM_CEVAL) or an application frame (in SCM_APPLY), if
;;; EITHER the `exit-frame' evaluator trap option is set, OR the
;;; `trace' debug option is set and the frame is marked as having been
;;; traced.  The frame will be marked as having been traced if the
;;; apply-frame handler was called for this frame.  (This is trickier
;;; than it sounds because of tail recursion: the same debug frame
;;; could have been used for multiple applications, only some of which
;;; were traced - I think.)  The arguments passed are:
;;;
;;; - the symbol 'exit-frame
;;;
;;; - a continuation or debug object describing the current stack
;;;
;;; - the result of the evaluation or application.

;;; {Stack IDs}
;;;
;;; Mechanism for limiting trapping to contexts whose stack ID matches
;;; one of a registered set.  The default set up is to limit trapping
;;; to events in the contexts of the Guile REPL and of file loading.

(define trapped-stack-ids (list 'repl-stack 'load-stack))
(define all-stack-ids-trapped? #f)

(define (add-trapped-stack-id! id)
  "Add ID to the set of stack ids for which traps are active.
If `#t' is in this set, traps are active regardless of stack context.
To remove ID again, use `remove-trapped-stack-id!'.  If you add the
same ID twice using `add-trapped-stack-id!', you will need to remove
it twice."
  (set! trapped-stack-ids (cons id trapped-stack-ids))
  (set! all-stack-ids-trapped? (memq #t trapped-stack-ids)))

(define (remove-trapped-stack-id! id)
  "Remove ID from the set of stack ids for which traps are active."
  (set! trapped-stack-ids (delq1! id trapped-stack-ids))
  (set! all-stack-ids-trapped? (memq #t trapped-stack-ids)))

(define (trap-here? cont)
  ;; Return true if the stack id of the specified continuation (or
  ;; debug object) is in the set that we should trap for; otherwise
  ;; false.
  (or all-stack-ids-trapped?
      (memq (stack-id cont) trapped-stack-ids)))

;;; {Global State}
;;;
;;; Variables tracking registered handlers, relevant procedures, and
;;; what's turned on as regards the evaluator's debugging options.

(define before-enter-frame-hook (make-hook 3))
(define enter-frame-hook (make-hook))
(define breakpoint-hook (make-hook))
(define after-enter-frame-hook (make-hook))

(define before-exit-frame-hook (make-hook 2))
(define exit-frame-hook (make-hook))
(define after-exit-frame-hook (make-hook))

(define before-apply-frame-hook (make-hook 2))
(define apply-frame-hook (make-hook))
(define trace-hook (make-hook))
(define after-apply-frame-hook (make-hook))

(define (hook-not-empty? hook)
  (not (hook-empty? hook)))

(define set-debug-and-trap-options
  (let ((dopts (debug-options))
	(topts (evaluator-traps-interface))
	(setting (lambda (key opts)
		   (let ((l (memq key opts)))
		     (and l
			  (not (null? (cdr l)))
			  (cadr l)))))
	(debug-set-boolean! (lambda (key value)
			      ((if value debug-enable debug-disable) key)))
	(trap-set-boolean! (lambda (key value)
			     ((if value trap-enable trap-disable) key))))
    (let ((save-debug (memq 'debug dopts))
	  (save-trace (memq 'trace dopts))
	  (save-breakpoints (memq 'breakpoints dopts))
	  (save-enter-frame (memq 'enter-frame topts))
	  (save-apply-frame (memq 'apply-frame topts))
	  (save-exit-frame (memq 'exit-frame topts))
	  (save-enter-frame-handler (setting 'enter-frame-handler topts))
	  (save-apply-frame-handler (setting 'apply-frame-handler topts))
	  (save-exit-frame-handler (setting 'exit-frame-handler topts)))
      (lambda ()
	(let ((need-trace (hook-not-empty? trace-hook))
	      (need-breakpoints (hook-not-empty? breakpoint-hook))
	      (need-enter-frame (hook-not-empty? enter-frame-hook))
	      (need-apply-frame (hook-not-empty? apply-frame-hook))
	      (need-exit-frame (hook-not-empty? exit-frame-hook)))
	  (debug-set-boolean! 'debug
			      (or need-trace
				  need-breakpoints
				  need-enter-frame
				  need-apply-frame
				  need-exit-frame
				  save-debug))
	  (debug-set-boolean! 'trace
			      (or need-trace
				  save-trace))
	  (debug-set-boolean! 'breakpoints
			      (or need-breakpoints
				  save-breakpoints))
	  (trap-set-boolean! 'enter-frame
			     (or need-enter-frame
				 save-enter-frame))
	  (trap-set-boolean! 'apply-frame
			     (or need-apply-frame
				 save-apply-frame))
	  (trap-set-boolean! 'exit-frame
			     (or need-exit-frame
				 save-exit-frame))
	  (trap-set! enter-frame-handler
		     (cond ((or need-breakpoints
				need-enter-frame)
			    enter-frame-handler)
			   (else save-enter-frame-handler)))
	  (trap-set! apply-frame-handler
		     (cond ((or need-trace
				need-apply-frame)
			    apply-frame-handler)
			   (else save-apply-frame-handler)))
	  (trap-set! exit-frame-handler
		     (cond ((or need-exit-frame)
			    exit-frame-handler)
			   (else save-exit-frame-handler))))
	;;(write (evaluator-traps-interface))
	*unspecified*))))

(define (enter-frame-handler key cont . args)
  ;; For a non-application entry, ARGS is (TAIL? EXP), where EXP is an
  ;; unmemoized copy of the source expression.  For an application
  ;; entry, ARGS is empty.
  (if (trap-here? cont)
      (let ((application-entry? (null? args)))
	(trc 'enter-frame-handler)
	(if application-entry?
	    (run-hook before-enter-frame-hook cont #f #f)
	    (run-hook before-enter-frame-hook cont (car args) (cadr args)))
	(run-hook enter-frame-hook)
	(or application-entry?
	    (run-hook breakpoint-hook))
	(run-hook after-enter-frame-hook))))

(define (exit-frame-handler key cont retval)
  (if (trap-here? cont)
      (begin
	(trc 'exit-frame-handler retval (stack-length (make-stack cont)))
	(run-hook before-exit-frame-hook cont retval)
	(run-hook exit-frame-hook)
	(run-hook after-exit-frame-hook))))

(define (apply-frame-handler key cont tail?)
  (if (trap-here? cont)
      (begin
	(trc 'apply-frame-handler tail?)
	(run-hook before-apply-frame-hook cont tail?)
	(run-hook apply-frame-hook)
	(run-hook trace-hook)
	(run-hook after-apply-frame-hook))))

(define-public (add-enter-frame-hook! proc)
  (add-hook! enter-frame-hook proc)
  (set-debug-and-trap-options))

(define-public (add-breakpoint-hook! proc)
  (add-hook! breakpoint-hook proc)
  (set-debug-and-trap-options))

(define-public (add-exit-frame-hook! proc)
  (add-hook! exit-frame-hook proc)
  (set-debug-and-trap-options))

(define-public (add-apply-frame-hook! proc)
  (add-hook! apply-frame-hook proc)
  (set-debug-and-trap-options))

(define-public (add-trace-hook! proc)
  (add-hook! trace-hook proc)
  (set-debug-and-trap-options))

(define-public (remove-enter-frame-hook! proc)
  (remove-hook! enter-frame-hook proc)
  (set-debug-and-trap-options))

(define-public (remove-breakpoint-hook! proc)
  (remove-hook! breakpoint-hook proc)
  (set-debug-and-trap-options))

(define-public (remove-exit-frame-hook! proc)
  (remove-hook! exit-frame-hook proc)
  (set-debug-and-trap-options))

(define-public (remove-apply-frame-hook! proc)
  (remove-hook! apply-frame-hook proc)
  (set-debug-and-trap-options))

(define-public (remove-trace-hook! proc)
  (remove-hook! trace-hook proc)
  (set-debug-and-trap-options))

(define-public (debug-hook-membership)
  (for-each (lambda (name+hook)
	      (format #t "~A:\n" (car name+hook))
	      (for-each (lambda (proc)
			  (format #t "  ~S\n" proc))
			(hook->list (cdr name+hook))))
	    `((before-enter-frame-hook . ,before-enter-frame-hook)
	      (enter-frame-hook	       . ,enter-frame-hook       )
	      (breakpoint-hook	       . ,breakpoint-hook        )
	      (after-enter-frame-hook  . ,after-enter-frame-hook )
	      (before-exit-frame-hook  . ,before-exit-frame-hook )
	      (exit-frame-hook	       . ,exit-frame-hook        )
	      (after-exit-frame-hook   . ,after-exit-frame-hook  )
	      (before-apply-frame-hook . ,before-apply-frame-hook)
	      (apply-frame-hook	       . ,apply-frame-hook       )
	      (trace-hook	       . ,trace-hook             )
	      (after-apply-frame-hook  . ,after-apply-frame-hook ))))

;;; (ice-9 debugger trap-hooks) ends here.
