;;;; (ice-9 debugging traps) -- abstraction of libguile's traps interface ;;; Copyright (C) 2002, 2004 Free Software Foundation, Inc. ;;; Copyright (C) 2005 Neil Jerram ;;; ;; 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 debugging traps) #:use-module (ice-9 regex) #:use-module (oop goops) #:use-module (oop goops describe) #:use-module (ice-9 debugging trc) #:use-module (srfi srfi-1) #:use-module (srfi srfi-2) #:export (tc:type tc:continuation tc:expression tc:return-value tc:stack tc:frame tc:depth tc:real-depth tc:exit-depth tc:fired-traps ;; Interface for users of subclasses defined in ;; this module. add-trapped-stack-id! remove-trapped-stack-id! install-trap uninstall-trap all-traps get-trap list-traps trap-ordering behaviour-ordering throw->trap-context on-pre-unwind-handler-dispatch ;; Interface for authors of new subclasses. trap->behaviour trap-runnable? install-apply-frame-trap install-breakpoint-trap install-enter-frame-trap install-exit-frame-trap install-trace-trap uninstall-apply-frame-trap uninstall-breakpoint-trap uninstall-enter-frame-trap uninstall-exit-frame-trap uninstall-trace-trap frame->source-position frame-file-name without-traps guile-trap-features) #:re-export (make) #:export-syntax (trap-here)) ;; How to debug the debugging infrastructure, when needed. Grep for ;; "(trc " to find other symbols that can be passed to trc-add. ;; (trc-add 'after-gc-hook) ;; In Guile 1.7 onwards, weak-vector and friends are provided by the ;; (ice-9 weak-vector) module. (cond ((string>=? (version) "1.7") (use-modules (ice-9 weak-vector)))) ;;; 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 possibly 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. ;;; {Trap Context} ;;; ;;; A trap context is a GOOPS object that encapsulates all the useful ;;; information about a particular trap. Encapsulating this ;;; information in a single object also allows us: ;;; ;;; - to defer the calculation of information that is time-consuming ;;; to calculate, such as the stack, and to cache such information so ;;; that it is only ever calculated once per trap ;;; ;;; - to pass all interesting information to trap behaviour procedures ;;; in a single parameter, which (i) is convenient and (ii) makes for ;;; a more future-proof interface. ;;; ;;; It also allows us - where very carefully documented! - to pass ;;; information from one behaviour procedure to another. (define-class () ;; Information provided directly by the trap calls from the ;; evaluator. The "type" slot holds a keyword indicating the type ;; of the trap: one of #:evaluation, #:application, #:return, ;; #:error. (type #:getter tc:type #:init-keyword #:type) ;; The "continuation" slot holds the continuation (or debug object, ;; if "cheap" traps are enabled, which is the default) at the point ;; of the trap. For an error trap it is #f. (continuation #:getter tc:continuation #:init-keyword #:continuation) ;; The "expression" slot holds the source code expression, for an ;; evaluation trap. (expression #:getter tc:expression #:init-keyword #:expression #:init-value #f) ;; The "return-value" slot holds the return value, for a return ;; trap, or the error args, for an error trap. (return-value #:getter tc:return-value #:init-keyword #:return-value #:init-value #f) ;; The list of trap objects which fired in this trap context. (fired-traps #:getter tc:fired-traps #:init-value '()) ;; The set of symbols which, if one of them is set in the CAR of the ;; handler-return-value slot, will cause the CDR of that slot to ;; have an effect. (handler-return-syms #:init-value '()) ;; The value which the trap handler should return to the evaluator. (handler-return-value #:init-value #f) ;; Calculated and cached information. "stack" is the stack ;; (computed from the continuation (or debug object) by make-stack, ;; or else (in the case of an error trap) by (make-stack #t ...). (stack #:init-value #f) (frame #:init-value #f) (depth #:init-value #f) (real-depth #:init-value #f) (exit-depth #:init-value #f)) (define-method (tc:stack (ctx )) (or (slot-ref ctx 'stack) (let ((stack (make-stack (tc:continuation ctx)))) (slot-set! ctx 'stack stack) stack))) (define-method (tc:frame (ctx )) (or (slot-ref ctx 'frame) (let ((frame (cond ((tc:continuation ctx) => last-stack-frame) (else (stack-ref (tc:stack ctx) 0))))) (slot-set! ctx 'frame frame) frame))) (define-method (tc:depth (ctx )) (or (slot-ref ctx 'depth) (let ((depth (stack-length (tc:stack ctx)))) (slot-set! ctx 'depth depth) depth))) (define-method (tc:real-depth (ctx )) (or (slot-ref ctx 'real-depth) (let* ((stack (tc:stack ctx)) (real-depth (apply + (map (lambda (i) (if (frame-real? (stack-ref stack i)) 1 0)) (iota (tc:depth ctx)))))) (slot-set! ctx 'real-depth real-depth) real-depth))) (define-method (tc:exit-depth (ctx )) (or (slot-ref ctx 'exit-depth) (let* ((stack (tc:stack ctx)) (depth (tc:depth ctx)) (exit-depth (let loop ((exit-depth depth)) (if (or (zero? exit-depth) (frame-real? (stack-ref stack (- depth exit-depth)))) exit-depth (loop (- exit-depth 1)))))) (slot-set! ctx 'exit-depth exit-depth) exit-depth))) ;;; {Stack IDs} ;;; ;;; Mechanism for limiting trapping to contexts whose stack ID matches ;;; one of a registered set. The default is for traps to fire ;;; regardless of stack ID. (define trapped-stack-ids (list #t)) (define all-stack-ids-trapped? #t) (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 enter-frame-traps '()) (define apply-frame-traps '()) (define exit-frame-traps '()) (define breakpoint-traps '()) (define trace-traps '()) (define (non-null? hook) (not (null? hook))) ;; The low level frame handlers must all be initialized to something ;; harmless. Otherwise we hit a problem immediately when trying to ;; enable one of these handlers. (trap-set! enter-frame-handler noop) (trap-set! apply-frame-handler noop) (trap-set! exit-frame-handler noop) (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 (non-null? trace-traps)) (need-breakpoints (non-null? breakpoint-traps)) (need-enter-frame (non-null? enter-frame-traps)) (need-apply-frame (non-null? apply-frame-traps)) (need-exit-frame (non-null? exit-frame-traps))) (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)) (trap-context (make #:type #:evaluation #:continuation cont #:expression (if application-entry? #f (cadr args))))) (trc 'enter-frame-handler) (if (and (not application-entry?) (memq 'tweaking guile-trap-features)) (slot-set! trap-context 'handler-return-syms '(instead))) (run-traps (if application-entry? enter-frame-traps (append enter-frame-traps breakpoint-traps)) trap-context) (slot-ref trap-context 'handler-return-value)))) (define (apply-frame-handler key cont tail?) (if (trap-here? cont) (let ((trap-context (make #:type #:application #:continuation cont))) (trc 'apply-frame-handler tail?) (run-traps (append apply-frame-traps trace-traps) trap-context) (slot-ref trap-context 'handler-return-value)))) (define (exit-frame-handler key cont retval) (if (trap-here? cont) (let ((trap-context (make #:type #:return #:continuation cont #:return-value retval))) (trc 'exit-frame-handler retval (tc:depth trap-context)) (if (memq 'tweaking guile-trap-features) (slot-set! trap-context 'handler-return-syms '(instead))) (run-traps exit-frame-traps trap-context) (slot-ref trap-context 'handler-return-value)))) (define-macro (trap-installer trap-list) `(lambda (trap) (set! ,trap-list (cons trap ,trap-list)) (set-debug-and-trap-options))) (define install-enter-frame-trap (trap-installer enter-frame-traps)) (define install-apply-frame-trap (trap-installer apply-frame-traps)) (define install-exit-frame-trap (trap-installer exit-frame-traps)) (define install-breakpoint-trap (trap-installer breakpoint-traps)) (define install-trace-trap (trap-installer trace-traps)) (define-macro (trap-uninstaller trap-list) `(lambda (trap) (or (memq trap ,trap-list) (error "Trap list does not include the specified trap")) (set! ,trap-list (delq1! trap ,trap-list)) (set-debug-and-trap-options))) (define uninstall-enter-frame-trap (trap-uninstaller enter-frame-traps)) (define uninstall-apply-frame-trap (trap-uninstaller apply-frame-traps)) (define uninstall-exit-frame-trap (trap-uninstaller exit-frame-traps)) (define uninstall-breakpoint-trap (trap-uninstaller breakpoint-traps)) (define uninstall-trace-trap (trap-uninstaller trace-traps)) (define trap-ordering (make-object-property)) (define behaviour-ordering (make-object-property)) (define (run-traps traps trap-context) (let ((behaviours (apply append (map (lambda (trap) (trap->behaviour trap trap-context)) (sort traps (lambda (t1 t2) (< (or (trap-ordering t1) 0) (or (trap-ordering t2) 0)))))))) (for-each (lambda (proc) (proc trap-context)) (sort (delete-duplicates behaviours) (lambda (b1 b2) (< (or (behaviour-ordering b1) 0) (or (behaviour-ordering b2) 0))))))) ;;; {Pseudo-Traps for Non-Trap Events} ;;; Once there is a body of code to do with responding to (debugging, ;;; tracing, etc.) traps, it makes sense to be able to leverage that ;;; same code for certain events that are trap-like, but not actually ;;; traps in the sense of the calls made by libguile's evaluator. ;;; The main example of this is when an error is signalled. Guile ;;; doesn't yet have a 100% reliable way of hooking into errors, but in ;;; practice most errors go through a catch whose pre-unwind handler is ;;; pre-unwind-handler-dispatch (defined in ice-9/boot-9.scm), which in ;;; turn calls default-pre-unwind-handler. So we can present most errors ;;; as pseudo-traps by modifying default-pre-unwind-handler. (define default-default-pre-unwind-handler default-pre-unwind-handler) (define (throw->trap-context key args . stack-args) (let ((ctx (make #:type #:error #:continuation #f #:return-value (cons key args)))) (slot-set! ctx 'stack (let ((caller-stack (and (= (length stack-args) 1) (car stack-args)))) (if (stack? caller-stack) caller-stack (apply make-stack #t stack-args)))) ctx)) (define (on-pre-unwind-handler-dispatch behaviour . ignored-keys) (set! default-pre-unwind-handler (if behaviour (lambda (key . args) (or (memq key ignored-keys) (behaviour (throw->trap-context key args pre-unwind-handler-dispatch))) (apply default-default-pre-unwind-handler key args)) default-default-pre-unwind-handler))) ;;; {Trap Classes} ;;; Class: ;;; ;;; is the base class for traps. Any actual trap should be an ;;; instance of a class derived from , not of itself, ;;; because there is no base class method for the install-trap, ;;; trap-runnable? and uninstall-trap GFs. (define-class () ;; "number" slot: the number of this trap (assigned automatically). (number) ;; "installed" slot: whether this trap is installed. (installed #:init-value #f) ;; "condition" slot: if non-#f, this is a thunk which is called when ;; the trap fires, to determine whether trap processing should ;; proceed any further. (condition #:init-value #f #:init-keyword #:condition) ;; "skip-count" slot: a count of valid (after "condition" ;; processing) firings of this trap to skip. (skip-count #:init-value 0 #:init-keyword #:skip-count) ;; "single-shot" slot: if non-#f, this trap is removed after it has ;; successfully fired (after "condition" and "skip-count" ;; processing) for the first time. (single-shot #:init-value #f #:init-keyword #:single-shot) ;; "behaviour" slot: procedure or list of procedures to call ;; (passing the trap context as parameter) if we finally decide ;; (after "condition" and "skip-count" processing) to run this ;; trap's behaviour. (behaviour #:init-value '() #:init-keyword #:behaviour) ;; "repeat-identical-behaviour" slot: normally, if multiple ;; objects are triggered by the same low level trap, and they ;; request the same behaviour, it's only useful to do that behaviour ;; once (per low level trap); so by default multiple requests for ;; the same behaviour are coalesced. If this slot is non-#f, the ;; contents of the "behaviour" slot are uniquified so that they ;; avoid being coalesced in this way. (repeat-identical-behaviour #:init-value #f #:init-keyword #:repeat-identical-behaviour) ;; "observer" slot: this is a procedure that is called with one ;; EVENT argument when the trap status changes in certain ;; interesting ways, currently the following. (1) When the trap is ;; uninstalled because of the target becoming inaccessible; EVENT in ;; this case is 'target-gone. (observer #:init-value #f #:init-keyword #:observer)) (define last-assigned-trap-number 0) (define all-traps (make-weak-value-hash-table 7)) (define-method (initialize (trap ) initargs) (next-method) ;; Assign a trap number, and store in the hash of all traps. (set! last-assigned-trap-number (+ last-assigned-trap-number 1)) (slot-set! trap 'number last-assigned-trap-number) (hash-set! all-traps last-assigned-trap-number trap) ;; Listify the behaviour slot, if not a list already. (let ((behaviour (slot-ref trap 'behaviour))) (if (procedure? behaviour) (slot-set! trap 'behaviour (list behaviour))))) (define-generic install-trap) ; provided mostly by subclasses (define-generic uninstall-trap) ; provided mostly by subclasses (define-generic trap->behaviour) ; provided by (define-generic trap-runnable?) ; provided by subclasses (define-method (install-trap (trap )) (if (slot-ref trap 'installed) (error "Trap is already installed")) (slot-set! trap 'installed #t)) (define-method (uninstall-trap (trap )) (or (slot-ref trap 'installed) (error "Trap is not installed")) (slot-set! trap 'installed #f)) ;;; uniquify-behaviour ;;; ;;; Uniquify BEHAVIOUR by wrapping it in a new lambda. (define (uniquify-behaviour behaviour) (lambda (trap-context) (behaviour trap-context))) ;;; trap->behaviour ;;; ;;; If TRAP is runnable, given TRAP-CONTEXT, return a list of ;;; behaviour procs to call with TRAP-CONTEXT as a parameter. ;;; Otherwise return the empty list. (define-method (trap->behaviour (trap ) (trap-context )) (if (and ;; Check that the trap is runnable. Runnability is implemented ;; by the subclass and allows us to check, for example, that ;; the procedure being applied in an apply-frame trap matches ;; this trap's procedure. (trap-runnable? trap trap-context) ;; Check the additional condition, if specified. (let ((condition (slot-ref trap 'condition))) (or (not condition) ((condition)))) ;; Check for a skip count. (let ((skip-count (slot-ref trap 'skip-count))) (if (zero? skip-count) #t (begin (slot-set! trap 'skip-count (- skip-count 1)) #f)))) ;; All checks passed, so we will return the contents of this ;; trap's behaviour slot. (begin ;; First, though, remove this trap if its single-shot slot ;; indicates that it should fire only once. (if (slot-ref trap 'single-shot) (uninstall-trap trap)) ;; Add this trap object to the context's list of traps which ;; fired here. (slot-set! trap-context 'fired-traps (cons trap (tc:fired-traps trap-context))) ;; Return trap behaviour, uniquified if necessary. (if (slot-ref trap 'repeat-identical-behaviour) (map uniquify-behaviour (slot-ref trap 'behaviour)) (slot-ref trap 'behaviour))) '())) ;;; Class: ;;; ;;; An installed instance of triggers on invocation ;;; of a specific procedure. (define-class () ;; "procedure" slot: the procedure to trap on. This is implemented ;; virtually, using the following weak vector slot, so as to avoid ;; this trap preventing the GC of the target procedure. (procedure #:init-keyword #:procedure #:allocation #:virtual #:slot-ref (lambda (trap) (vector-ref (slot-ref trap 'procedure-wv) 0)) #:slot-set! (lambda (trap proc) (if (slot-bound? trap 'procedure-wv) (vector-set! (slot-ref trap 'procedure-wv) 0 proc) (slot-set! trap 'procedure-wv (weak-vector proc))))) (procedure-wv)) ;; Customization of the initialize method: set up to handle what ;; should happen when the procedure is GC'd. (define-method (initialize (trap ) initargs) (next-method) (let* ((proc (slot-ref trap 'procedure)) (existing-traps (volatile-target-traps proc))) ;; If this is the target's first trap, give the target procedure ;; to the volatile-target-guardian, so we can find out if it ;; becomes inaccessible. (or existing-traps (volatile-target-guardian proc)) ;; Add this trap to the target procedure's list of traps. (set! (volatile-target-traps proc) (cons trap (or existing-traps '()))))) (define procedure-trace-count (make-object-property)) (define-method (install-trap (trap )) (next-method) (let* ((proc (slot-ref trap 'procedure)) (trace-count (or (procedure-trace-count proc) 0))) (set-procedure-property! proc 'trace #t) (set! (procedure-trace-count proc) (+ trace-count 1))) (install-trace-trap trap)) (define-method (uninstall-trap (trap )) (next-method) (let* ((proc (slot-ref trap 'procedure)) (trace-count (or (procedure-trace-count proc) 0))) (if (= trace-count 1) (set-procedure-property! proc 'trace #f)) (set! (procedure-trace-count proc) (- trace-count 1))) (uninstall-trace-trap trap)) (define-method (trap-runnable? (trap ) (trap-context )) (eq? (slot-ref trap 'procedure) (frame-procedure (tc:frame trap-context)))) ;;; Class: ;;; ;;; An installed instance of triggers on stack frame exit ;;; past a specified stack depth. (define-class () ;; "depth" slot: the reference depth for the trap. (depth #:init-keyword #:depth)) (define-method (install-trap (trap )) (next-method) (install-exit-frame-trap trap)) (define-method (uninstall-trap (trap )) (next-method) (uninstall-exit-frame-trap trap)) (define-method (trap-runnable? (trap ) (trap-context )) (<= (tc:exit-depth trap-context) (slot-ref trap 'depth))) ;;; Class: ;;; ;;; An installed instance of triggers on any frame entry. (define-class ()) (define-method (install-trap (trap )) (next-method) (install-enter-frame-trap trap)) (define-method (uninstall-trap (trap )) (next-method) (uninstall-enter-frame-trap trap)) (define-method (trap-runnable? (trap ) (trap-context )) #t) ;;; Class: ;;; ;;; An installed instance of triggers on any procedure ;;; application. (define-class ()) (define-method (install-trap (trap )) (next-method) (install-apply-frame-trap trap)) (define-method (uninstall-trap (trap )) (next-method) (uninstall-apply-frame-trap trap)) (define-method (trap-runnable? (trap ) (trap-context )) #t) ;;; Class: ;;; ;;; An installed instance of triggers on the next frame ;;; entry, exit or application, optionally with source location inside ;;; a specified file. (define-class () ;; "file-name" slot: if non-#f, indicates that this trap should ;; trigger only for steps in source code from the specified file. (file-name #:init-value #f #:init-keyword #:file-name) ;; "exit-depth" slot: when non-#f, indicates that the next step may ;; be a frame exit past this depth; otherwise, indicates that the ;; next step must be an application or a frame entry. (exit-depth #:init-value #f #:init-keyword #:exit-depth)) (define-method (initialize (trap ) initargs) (next-method) (slot-set! trap 'depth (slot-ref trap 'exit-depth))) (define-method (install-trap (trap )) (next-method) (install-enter-frame-trap trap) (install-apply-frame-trap trap)) (define-method (uninstall-trap (trap )) (next-method) (uninstall-enter-frame-trap trap) (uninstall-apply-frame-trap trap)) (define-method (trap-runnable? (trap ) (trap-context )) (if (eq? (tc:type trap-context) #:return) ;; We're in the context of an exit-frame trap. Trap should only ;; be run if exit-depth is set and this exit-frame has returned ;; past the set depth. (and (slot-ref trap 'exit-depth) (next-method) ;; OK to run the trap here, but we should first reset the ;; exit-depth slot to indicate that the step after this one ;; must be an application or frame entry. (begin (slot-set! trap 'exit-depth #f) #t)) ;; We're in the context of an application or frame entry trap. ;; Check whether trap is limited to a specified file. (let ((file-name (slot-ref trap 'file-name))) (and (or (not file-name) (equal? (frame-file-name (tc:frame trap-context)) file-name)) ;; Trap should run here, but we should also set exit-depth to ;; the current stack length, so that - if we don't stop at any ;; other steps first - the next step shows the return value of ;; the current application or evaluation. (begin (slot-set! trap 'exit-depth (tc:depth trap-context)) (slot-set! trap 'depth (tc:depth trap-context)) #t))))) (define (frame->source-position frame) (let ((source (if (frame-procedure? frame) (or (frame-source frame) (let ((proc (frame-procedure frame))) (and proc (procedure? proc) (procedure-source proc)))) (frame-source frame)))) (and source (string? (source-property source 'filename)) (list (source-property source 'filename) (source-property source 'line) (source-property source 'column))))) (define (frame-file-name frame) (cond ((frame->source-position frame) => car) (else #f))) ;;; Class: ;;; ;;; An installed instance of triggers upon evaluation of ;;; a specified source expression. (define-class () ;; "expression" slot: the expression to trap on. This is ;; implemented virtually, using the following weak vector slot, so ;; as to avoid this trap preventing the GC of the target source ;; code. (expression #:init-keyword #:expression #:allocation #:virtual #:slot-ref (lambda (trap) (vector-ref (slot-ref trap 'expression-wv) 0)) #:slot-set! (lambda (trap expr) (if (slot-bound? trap 'expression-wv) (vector-set! (slot-ref trap 'expression-wv) 0 expr) (slot-set! trap 'expression-wv (weak-vector expr))))) (expression-wv) ;; source property slots - for internal use only (filename) (line) (column)) ;; Customization of the initialize method: get and save the ;; expression's source properties, or signal an error if it doesn't ;; have the necessary properties. (define-method (initialize (trap ) initargs) (next-method) (let* ((expr (slot-ref trap 'expression)) (filename (source-property expr 'filename)) (line (source-property expr 'line)) (column (source-property expr 'column)) (existing-traps (volatile-target-traps expr))) (or (and filename line column) (error "Specified source does not have the necessary properties" filename line column)) (slot-set! trap 'filename filename) (slot-set! trap 'line line) (slot-set! trap 'column column) ;; If this is the target's first trap, give the target expression ;; to the volatile-target-guardian, so we can find out if it ;; becomes inaccessible. (or existing-traps (volatile-target-guardian expr)) ;; Add this trap to the target expression's list of traps. (set! (volatile-target-traps expr) (cons trap (or existing-traps '()))))) ;; Just in case more than one trap is installed on the same source ;; expression ... so that we can still get the setting and resetting ;; of the 'breakpoint source property correct. (define source-breakpoint-count (make-object-property)) (define-method (install-trap (trap )) (next-method) (let* ((expr (slot-ref trap 'expression)) (breakpoint-count (or (source-breakpoint-count expr) 0))) (set-source-property! expr 'breakpoint #t) (set! (source-breakpoint-count expr) (+ breakpoint-count 1))) (install-breakpoint-trap trap)) (define-method (uninstall-trap (trap )) (next-method) (let* ((expr (slot-ref trap 'expression)) (breakpoint-count (or (source-breakpoint-count expr) 0))) (if (= breakpoint-count 1) (set-source-property! expr 'breakpoint #f)) (set! (source-breakpoint-count expr) (- breakpoint-count 1))) (uninstall-breakpoint-trap trap)) (define-method (trap-runnable? (trap ) (trap-context )) (or (eq? (slot-ref trap 'expression) (tc:expression trap-context)) (let ((trap-location (frame->source-position (tc:frame trap-context)))) (and trap-location (string=? (car trap-location) (slot-ref trap 'filename)) (= (cadr trap-location) (slot-ref trap 'line)) (= (caddr trap-location) (slot-ref trap 'column)))))) ;; (trap-here EXPRESSION . OPTIONS) (define trap-here (procedure->memoizing-macro (lambda (expr env) (let ((trap (apply make #:expression expr (local-eval `(list ,@(cddr expr)) env)))) (install-trap trap) (set-car! expr 'begin) (set-cdr! (cdr expr) '()) expr)))) ;;; Class: ;;; ;;; An installed instance of triggers on entry to a ;;; frame with a more-or-less precisely specified source location. (define-class () ;; "file-regexp" slot: regexp matching the name(s) of the file(s) to ;; trap in. (file-regexp #:init-keyword #:file-regexp) ;; "line" and "column" slots: position to trap at (0-based). (line #:init-value #f #:init-keyword #:line) (column #:init-value #f #:init-keyword #:column) ;; "compiled-regexp" slot - self explanatory, internal use only (compiled-regexp)) (define-method (initialize (trap ) initargs) (next-method) (slot-set! trap 'compiled-regexp (make-regexp (slot-ref trap 'file-regexp)))) (define-method (install-trap (trap )) (next-method) (install-enter-frame-trap trap)) (define-method (uninstall-trap (trap )) (next-method) (uninstall-enter-frame-trap trap)) (define-method (trap-runnable? (trap ) (trap-context )) (and-let* ((trap-location (frame->source-position (tc:frame trap-context))) (tcline (cadr trap-location)) (tccolumn (caddr trap-location))) (and (= tcline (slot-ref trap 'line)) (= tccolumn (slot-ref trap 'column)) (regexp-exec (slot-ref trap 'compiled-regexp) (car trap-location) 0)))) ;;; {Misc Trap Utilities} (define (get-trap number) (hash-ref all-traps number)) (define (list-traps) (for-each describe (map cdr (sort (hash-fold acons '() all-traps) (lambda (x y) (< (car x) (car y))))))) ;;; {Volatile Traps} ;;; ;;; Some traps are associated with Scheme objects that are likely to ;;; be GC'd, such as procedures and read expressions. When those ;;; objects are GC'd, we want to allow their traps to evaporate as ;;; well, or at least not to prevent them from doing so because they ;;; are (now pointlessly) included on the various installed trap ;;; lists. ;; An object property that maps each volatile target to the list of ;; traps that are installed on it. (define volatile-target-traps (make-object-property)) ;; A guardian that tells us when a volatile target is no longer ;; accessible. (define volatile-target-guardian (make-guardian)) ;; An after GC hook that checks for newly inaccessible targets. (add-hook! after-gc-hook (lambda () (trc 'after-gc-hook) (let loop ((target (volatile-target-guardian))) (if target ;; We have a target which is now inaccessible. Get ;; the list of traps installed on it. (begin (trc 'after-gc-hook "got target") ;; Uninstall all the traps that are installed on ;; this target. (for-each (lambda (trap) (trc 'after-gc-hook "got trap") ;; If the trap is still installed, ;; uninstall it. (if (slot-ref trap 'installed) (uninstall-trap trap)) ;; If the trap has an observer, tell ;; it that the target has gone. (cond ((slot-ref trap 'observer) => (lambda (proc) (trc 'after-gc-hook "call obs") (proc 'target-gone))))) (or (volatile-target-traps target) '())) ;; Check for any more inaccessible targets. (loop (volatile-target-guardian))))))) (define (without-traps thunk) (with-traps (lambda () (trap-disable 'traps) (thunk)))) (define guile-trap-features ;; Helper procedure, to test whether a specific possible Guile ;; feature is supported. (let ((supported? (lambda (test-feature) (case test-feature ((tweaking) ;; Tweaking is supported if the description of the cheap ;; traps option includes the word "obsolete", or if the ;; option isn't there any more. (and (string>=? (version) "1.7") (let ((cheap-opt-desc (assq 'cheap (debug-options-interface 'help)))) (or (not cheap-opt-desc) (string-match "obsolete" (caddr cheap-opt-desc)))))) (else (error "Unexpected feature name:" test-feature)))))) ;; Compile the list of actually supported features from all ;; possible features. (let loop ((possible-features '(tweaking)) (actual-features '())) (if (null? possible-features) (reverse! actual-features) (let ((test-feature (car possible-features))) (loop (cdr possible-features) (if (supported? test-feature) (cons test-feature actual-features) actual-features))))))) ;; Make sure that traps are enabled. (trap-enable 'traps) ;;; (ice-9 debugging traps) ends here.