cl-debugger-hook global (nil = default), cl-invoke-debugger walks the hook, cl-error-with-debugger routes unhandled errors through the hook, and make-policy-debugger builds a hook from a (fn (condition restarts) name) policy function. Tests: hook receives condition, policy selects use-zero/abort restarts, compute-restarts visible inside hook, handler wins before hook fires, infinite-recursion guard. Wired into test.sh program suite runner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
196 lines
6.4 KiB
Plaintext
196 lines
6.4 KiB
Plaintext
;; interactive-debugger.sx — Condition debugger using *debugger-hook*
|
|
;;
|
|
;; Demonstrates the classic CL debugger pattern:
|
|
;; - *debugger-hook* is invoked when an unhandled error reaches the top level
|
|
;; - The hook receives the condition and a reference to itself
|
|
;; - It can offer restarts interactively (here simulated with a policy fn)
|
|
;;
|
|
;; In real CL the debugger reads from the terminal. Here we simulate
|
|
;; the "user input" via a policy function passed in at call time.
|
|
;;
|
|
;; Depends on: lib/common-lisp/runtime.sx already loaded.
|
|
|
|
;; ── *debugger-hook* global ────────────────────────────────────────────────
|
|
;;
|
|
;; CL: when error is unhandled, invoke *debugger-hook* with (condition hook).
|
|
;; A nil hook means use the system default (which we simulate as re-raise).
|
|
|
|
(define cl-debugger-hook nil)
|
|
|
|
;; ── invoke-debugger ────────────────────────────────────────────────────────
|
|
;;
|
|
;; Called when cl-error finds no handler. Tries cl-debugger-hook first;
|
|
;; falls back to a simple error report.
|
|
|
|
(define
|
|
cl-invoke-debugger
|
|
(fn
|
|
(c)
|
|
(if
|
|
(nil? cl-debugger-hook)
|
|
(error (str "Debugger: " (cl-condition-message c)))
|
|
(begin
|
|
(let
|
|
((hook cl-debugger-hook))
|
|
(set! cl-debugger-hook nil)
|
|
(let
|
|
((result (hook c hook)))
|
|
(set! cl-debugger-hook hook)
|
|
result))))))
|
|
|
|
;; ── cl-error/debugger — error that routes through invoke-debugger ─────────
|
|
|
|
(define
|
|
cl-error-with-debugger
|
|
(fn
|
|
(c &rest args)
|
|
(let
|
|
((obj (cond ((cl-condition? c) c) ((string? c) (cl-make-condition "simple-error" "format-control" c "format-arguments" args)) (:else (cl-make-condition "simple-error" "format-control" (str c))))))
|
|
(cl-signal-obj obj cl-handler-stack)
|
|
(cl-invoke-debugger obj))))
|
|
|
|
;; ── simulated debugger session ────────────────────────────────────────────
|
|
;;
|
|
;; A debugger hook takes (condition hook) and "reads" user commands.
|
|
;; We simulate this with a policy function: (fn (c restarts) restart-name)
|
|
;; that picks a restart given the condition and available restarts.
|
|
|
|
(define
|
|
make-policy-debugger
|
|
(fn
|
|
(policy)
|
|
(fn
|
|
(c hook)
|
|
(let
|
|
((available (cl-compute-restarts)))
|
|
(let
|
|
((choice (policy c available)))
|
|
(if
|
|
(and choice (not (nil? (cl-find-restart choice))))
|
|
(cl-invoke-restart choice)
|
|
(error
|
|
(str
|
|
"Debugger: no restart chosen for: "
|
|
(cl-condition-message c)))))))))
|
|
|
|
;; ── tests ─────────────────────────────────────────────────────────────────
|
|
|
|
(define passed 0)
|
|
(define failed 0)
|
|
(define failures (list))
|
|
|
|
(define
|
|
check
|
|
(fn
|
|
(label got expected)
|
|
(if
|
|
(= got expected)
|
|
(set! passed (+ passed 1))
|
|
(begin
|
|
(set! failed (+ failed 1))
|
|
(set!
|
|
failures
|
|
(append
|
|
failures
|
|
(list
|
|
(str
|
|
"FAIL ["
|
|
label
|
|
"]: got="
|
|
(inspect got)
|
|
" expected="
|
|
(inspect expected)))))))))
|
|
|
|
(define
|
|
reset-stacks!
|
|
(fn
|
|
()
|
|
(set! cl-handler-stack (list))
|
|
(set! cl-restart-stack (list))
|
|
(set! cl-debugger-hook nil)))
|
|
|
|
;; Test 1: debugger hook receives condition
|
|
(reset-stacks!)
|
|
(let
|
|
((received-msg ""))
|
|
(begin
|
|
(set!
|
|
cl-debugger-hook
|
|
(fn (c hook) (set! received-msg (cl-condition-message c)) nil))
|
|
(cl-restart-case
|
|
(fn () (cl-error-with-debugger "something broke"))
|
|
(list "abort" (list) (fn () nil)))
|
|
(check "debugger hook receives condition" received-msg "something broke")))
|
|
|
|
;; Test 2: policy-driven restart selection (use-zero)
|
|
(reset-stacks!)
|
|
(let
|
|
((result (begin (set! cl-debugger-hook (make-policy-debugger (fn (c restarts) "use-zero"))) (cl-restart-case (fn () (cl-error-with-debugger (cl-make-condition "division-by-zero")) 999) (list "use-zero" (list) (fn () 0))))))
|
|
(check "policy debugger: use-zero restart" result 0))
|
|
|
|
;; Test 3: policy selects abort
|
|
(reset-stacks!)
|
|
(let
|
|
((result (begin (set! cl-debugger-hook (make-policy-debugger (fn (c restarts) "abort"))) (cl-restart-case (fn () (cl-error-with-debugger "aborting error") 999) (list "abort" (list) (fn () "aborted"))))))
|
|
(check "policy debugger: abort restart" result "aborted"))
|
|
|
|
;; Test 4: compute-restarts inside debugger hook
|
|
(reset-stacks!)
|
|
(let
|
|
((seen-restarts (list)))
|
|
(begin
|
|
(set!
|
|
cl-debugger-hook
|
|
(fn
|
|
(c hook)
|
|
(set! seen-restarts (cl-compute-restarts))
|
|
(cl-invoke-restart "continue")))
|
|
(cl-restart-case
|
|
(fn () (cl-error-with-debugger "test") 42)
|
|
(list "continue" (list) (fn () "ok"))
|
|
(list "abort" (list) (fn () "no")))
|
|
(check
|
|
"debugger: compute-restarts visible"
|
|
(= (len seen-restarts) 2)
|
|
true)))
|
|
|
|
;; Test 5: hook not invoked when handler catches first
|
|
(reset-stacks!)
|
|
(let
|
|
((hook-called false)
|
|
(result
|
|
(begin
|
|
(set! cl-debugger-hook (fn (c hook) (set! hook-called true) nil))
|
|
(cl-handler-case
|
|
(fn () (cl-error-with-debugger "handled"))
|
|
(list "error" (fn (c) "handler-won"))))))
|
|
(check "handler wins; hook not called" hook-called false)
|
|
(check "handler result returned" result "handler-won"))
|
|
|
|
;; Test 6: debugger-hook nil after re-raise guard
|
|
(reset-stacks!)
|
|
(let
|
|
((hook-calls 0))
|
|
(begin
|
|
(set!
|
|
cl-debugger-hook
|
|
(fn
|
|
(c hook)
|
|
(set! hook-calls (+ hook-calls 1))
|
|
(if
|
|
(> hook-calls 1)
|
|
(error "infinite loop guard")
|
|
(cl-invoke-restart "escape"))))
|
|
(cl-restart-case
|
|
(fn () (cl-error-with-debugger "once"))
|
|
(list "escape" (list) (fn () nil)))
|
|
(check
|
|
"hook called exactly once (no infinite recursion)"
|
|
hook-calls
|
|
1)))
|
|
|
|
;; ── summary ────────────────────────────────────────────────────────────────
|
|
|
|
(define debugger-passed passed)
|
|
(define debugger-failed failed)
|
|
(define debugger-failures failures) |