Browse files

Experimental step debugger

This arises from me taking a fresh look at the years-old debugger
branch that I never merged.

Compared to back then, now that the command mechanism has changed,
it's simpler to connect the front and back ends. We have a thread that
reads command responses from a channel and writes them as sexprs to
the TCP port. We simply extend this to also sync on a
debug-break-channel and write those, too. Up in Emacs, we look for
these (debug-break ___) sexprs -- easy to distinguish from (ok __)
and (error __) sexprs that are command responses -- and dispatch them
directly to a dedicated singleton callback function.

Like DrRacket this allows entire-file debugging. Unlike DrRacket:

- You're not prompted you about each file interactively; instead it
  uses the Emacs Lisp variable `racket-debuggable-files'.

- `racket-repl-mode' provides a debug prompt at each step, in which
  locals are available to inspect and even set!.

- You can do an edebug style C-u C-M-x to debug instrument individual
  function definitions.

A new `racket-debug-mode' minor mode provides visual feedback and
resume commands in the `racket-mode' buffer where the break happened.

I did need to "fork" gui-debugger/annotate to resolve two issues
I reported there:


Even if those are fixed, there, racket-mode is supposed to support
Racket versions as old as 6.0, so I think my fork will need to remain.
  • Loading branch information...
greghendershott committed Aug 16, 2018
1 parent 29e58e1 commit 2b1c7d476dc71b1707fd5222f963ab6509e50805
Showing with 1,286 additions and 98 deletions.
  1. +101 −2
  2. +14 −13 channel.rkt
  3. +41 −27 cmds.rkt
  4. +381 −0 debug-annotator.rkt
  5. +310 −0 debug.rkt
  6. +3 −7 instrument.rkt
  7. +2 −1 interactions.rkt
  8. +1 −0 makefile
  9. +15 −0 racket-custom.el
  10. +319 −0 racket-debug.el
  11. +20 −10 racket-edit.el
  12. +5 −1 racket-make-doc.el
  13. +2 −1 racket-mode.el
  14. +39 −19 racket-repl.el
  15. +15 −16 run.rkt
  16. +18 −1 util.rkt
@@ -13,6 +13,7 @@
- [General](#general)
- [REPL](#repl)
- [Other](#other)
- [Experimental debugger](#experimental-debugger)
- [Faces](#faces)
# Commands
@@ -22,11 +23,14 @@
### racket-run
<kbd>C-c C-k</kbd> or <kbd>C-c C-c</kbd>
Save and evaluate the buffer in REPL, much like DrRacket's Run.
Save and evaluate the buffer in REPL.
With a C-u prefix, uses errortrace for improved stack traces.
With one C-u prefix, uses errortrace for improved stack traces.
Otherwise follows the [`racket-error-context`](#racket-error-context) setting.
With two C-u prefixes, instruments code for step debugging. See
[`racket-debug-mode`](#racket-debug-mode) and the variable [`racket-debuggable-files`](#racket-debuggable-files).
If point is within a Racket `module` form, the REPL "enters"
that submodule (uses its language info and namespace).
@@ -152,6 +156,88 @@ In addition to any hooks its parent mode `special-mode` might have run,
this mode runs the hook [`racket-logger-mode-hook`](#racket-logger-mode-hook), as the final or penultimate step
during initialization.
### racket-debug-mode
<kbd>M-x racket-debug-mode</kbd>
Minor mode for debug breaks.
> This feature is **EXPERIMENTAL**!!! It is likely to have
> significant limitations and bugs. You are welcome to open an
> issue to provide feedback. Please understand that this feature
> might never be improved -- it might even be removed someday if
> it turns out to have too little value and/or too much cost.
How to debug:
1. "Instrument" code for step debugging. You can instrument
entire files, and also individual functions.
a. Entire Files
Choose [`racket-run`](#racket-run) with two prefixes -- C-u C-u C-c C-c. The
file will be instrumented for step debugging before it is run.
Also instrumented are files determined by the variable
The run will break at the first breakable position.
Tip: After you run to completion and return to a normal
REPL prompt, the code remains instrumented. You may enter
expressions that evaluate instrumented code and it will
break so you can step debug again.
b. Function Definitions
Put point in a function `define` form and C-u C-M-x to
"instrument" the function for step debugging. Then in the
REPL, enter an expression that causes the instrumented
function to be run, directly or indirectly.
You can instrument any number of functions.
You can even instrument while stopped at a break. For
example, to instrument a function you are about to call, so
you can "step into" it:
- M-. a.k.a. [`racket-visit-definition`](#racket-visit-definition).
- C-u C-M-x to instrument the definition.
- M-, a.k.a. [`racket-unvisit`](#racket-unvisit).
- Continue stepping.
Limitation: Instrumenting a function `require`d from
another module won't redefine that function. Instead, it
attempts to define an instrumented function of the same
name, in the module the REPL is inside. The define will
fail if it needs definitions visible only in that other
module. In that case you'll probably need to use
entire-file instrumentation as described above.
2. When a break occurs, the [`racket-repl-mode`](#racket-repl-mode) prompt changes. In
this debug REPL, local variables are available for you to use
and even to `set!`.
Also, in the [`racket-mode`](#racket-mode) buffer where the break is located,
[`racket-debug-mode`](#racket-debug-mode) is enabled. This minor mode makes the
buffer read-only, provides visual feedback -- about the break
position, local variable values, and result values -- and
provides shortcut keys:
key binding
--- -------
SPC racket-debug-step
? racket-debug-help
c racket-debug-continue
h racket-debug-run-to-here
n racket-debug-next-breakable
o racket-debug-step-over
p racket-debug-prev-breakable
u racket-debug-step-out
## Test
### racket-test
@@ -899,6 +985,19 @@ The default value sets some known "noisy" topics to be one
level quieter. That way you can set the '* topic to a level like
'debug and not get overhwelmed by these noisy topics.
## Experimental debugger
### racket-debuggable-files
Used to tell [`racket-run`](#racket-run) what files may be instrumented for debugging.
Must be a list of strings that are pathnames, such as from
[`racket--buffer-file-name`](#racket--buffer-file-name), -or-, a function that returns such a
list given the pathname of the file being run. If any path
strings are relative, they are made absolute using
`expand-file-name` with the directory of the file being run. The
symbol 'run-file may be supplied in the list; it will be replaced
with the pathname of the file being run. Safe to set as a
file-local variable.
# Faces
> Note: You may also set these via Customize.
@@ -1,7 +1,8 @@
#lang racket/base
(require racket/match
(require racket/contract
(provide message-to-main-thread-channel
@@ -11,7 +12,8 @@
;;; Definitions for the context-level member of rerun
@@ -28,19 +30,16 @@
(define context-levels
`(low ;compile-context-preservation-enabled #f
medium ;compile-context-preservation-enabled #t
(define-syntax-rule (memq? x xs)
(not (not (memq x xs))))
(and (memq x xs) #t))
(define (context-level? v)
(memq? v context-levels))
(define (instrument-level? v)
(memq? v instrument-levels))
(define (profile/coverage-level? v)
(memq? v profile/coverage-levels))
(define (context-level? v) (memq? v context-levels))
(define (instrument-level? v) (memq? v instrument-levels))
(define (profile/coverage-level? v) (memq? v profile/coverage-levels))
(define (debug-level? v) (eq? v 'debug))
;;; Messages to the main thread via a channel
@@ -61,11 +60,13 @@
;; 6.1 when the value is accessed from the struct and passed to
;; `current-command-line-arguments`. WAT.
[cmd-line-args vector?]
[debug-files (set/c path?)]
[ready-thunk (-> any/c)]))
(define rerun-default (rerun #f
@@ -18,6 +18,7 @@
(only-in xml xexpr->string)
@@ -32,7 +33,9 @@
(provide start-command-server
(module+ test
(require rackunit))
@@ -84,6 +87,7 @@
(define/contract (attach-command-server ns maybe-mod)
(-> namespace? (or/c #f mod?) any)
(set-debug-repl-namespace! ns)
(set! command-server-context
(context ns
@@ -160,7 +164,9 @@
(context-ns command-server-context)])
`(ok ,(command sexp command-server-context)))))))
(define (get/write-response)
(elisp-writeln (channel-get response-channel) out)
(elisp-writeln (sync response-channel
(flush-output out)
;; With all the pieces defined, let's go:
@@ -176,7 +182,8 @@
(define/contract ((make-prompt-read m))
(-> (or/c #f mod?) (-> any))
(get-interaction (maybe-mod->prompt-string m)))
(begin0 (get-interaction (maybe-mod->prompt-string m))
(next-break 'all))) ;let debug-instrumented code break again
(define/contract (command sexpr the-context)
(-> pair? context? any/c)
@@ -186,26 +193,29 @@
;; Note: Intentionally no "else" match clause -- let caller handle
;; exn and supply a consistent exn response format.
(match sexpr
[`(run ,what ,mem ,pp? ,ctx ,args) (run what mem pp? ctx args)]
[`(path+md5) (cons (or path 'top) md5)]
[`(syms) (syms)]
[`(def ,str) (find-definition str)]
[`(mod ,sym) (find-module sym maybe-mod)]
[`(describe ,str) (describe str)]
[`(doc ,str) (doc str)]
[`(type ,v) (type v)]
[`(macro-stepper ,str ,into-base?) (macro-stepper str into-base?)]
[`(macro-stepper/next) (macro-stepper/next)]
[`(requires/tidy ,reqs) (requires/tidy reqs)]
[`(requires/trim ,path-str ,reqs) (requires/trim path-str reqs)]
[`(requires/base ,path-str ,reqs) (requires/base path-str reqs)]
[`(find-collection ,str) (find-collection str)]
[`(get-profile) (get-profile)]
[`(get-uncovered) (get-uncovered path)]
[`(check-syntax ,path-str) (check-syntax path-str)]
[`(eval ,v) (eval-command v)]
[`(repl-submit? ,str ,eos?) (repl-submit? submit-pred str eos?)]
[`(exit) (exit)]))
[`(run ,what ,mem ,pp? ,ctx ,args ,dbg) (run what mem pp? ctx args dbg)]
[`(path+md5) (cons (or path 'top) md5)]
[`(syms) (syms)]
[`(def ,str) (find-definition str)]
[`(mod ,sym) (find-module sym maybe-mod)]
[`(describe ,str) (describe str)]
[`(doc ,str) (doc str)]
[`(type ,v) (type v)]
[`(macro-stepper ,str ,into-base?) (macro-stepper str into-base?)]
[`(macro-stepper/next) (macro-stepper/next)]
[`(requires/tidy ,reqs) (requires/tidy reqs)]
[`(requires/trim ,path-str ,reqs) (requires/trim path-str reqs)]
[`(requires/base ,path-str ,reqs) (requires/base path-str reqs)]
[`(find-collection ,str) (find-collection str)]
[`(get-profile) (get-profile)]
[`(get-uncovered) (get-uncovered path)]
[`(check-syntax ,path-str) (check-syntax path-str)]
[`(eval ,v) (eval-command v)]
[`(repl-submit? ,str ,eos?) (repl-submit? submit-pred str eos?)]
[`(debug-eval ,src ,l ,c ,p ,code) (debug-eval src l c p code)]
[`(debug-resume ,v) (debug-resume v)]
[`(debug-disable) (debug-disable)]
[`(exit) (exit)]))
;;; read/write Emacs Lisp values
@@ -241,18 +251,21 @@
[(? list? xs) (map racket->elisp xs)]
[(cons x y) (cons (racket->elisp x) (racket->elisp y))]
[(? path? v) (path->string v)]
[(? hash? v) (for/list ([(k v) (in-hash v)])
(cons (racket->elisp k) (racket->elisp v)))]
[(? set? v) (map racket->elisp (set->list v))]
[v v]))
(module+ test
(check-equal? (with-output-to-string
(λ () (elisp-write '(1 #t nil () (a . b))
(λ () (elisp-write '(1 #t nil () (a . b) #hash((1 . 2) (3 . 4)))
"(1 t nil nil (a . b))"))
"(1 t nil nil (a . b) ((1 . 2) (3 . 4)))"))
;;; commands
(define/contract (run what mem pp ctx args)
(-> list? number? elisp-bool/c context-level? list?
(define/contract (run what mem pp ctx args dbgs)
(-> list? number? elisp-bool/c context-level? list? (listof path-string?)
(define ready-channel (make-channel))
(channel-put message-to-main-thread-channel
@@ -261,6 +274,7 @@
(as-racket-bool pp)
(list->vector args)
(list->set (map string->path dbgs))
(λ () (channel-put ready-channel what))))
;; Waiting for this allows the command response to be used as the
;; all-clear for additional commands that need the module load to be
Oops, something went wrong.

0 comments on commit 2b1c7d4

Please sign in to comment.