-*- mode:org ; mode:visual-line -*-
Hack && Tell 6 Boston
Specifically a “step debugger”?
A program that runs your program, letting you
- Pause execution (“break”) at steps of its evaluation (“breakpoints”)
- Examine or set values
Depends on programming language.
Assembly Values of registers, memory, and stack (on most architectures, memory and stack are two sides of same coin).
C/C++ Global and local variables (memory and stack)
Racket Global and local variables (???)
Depends on your point of view.
A CPU runs one “stream of code” from power-up to power-down.
To it, “a debugger running your program” is one program.
In some sense, “running your program under a debugger” means “running your program tweaked to be debuggable”.
When you set a breakpoint, it replaces your program’s instruction there with a special CPU instruction like “INT <n>”.
The CPU treats INT <n> as, “run some code at the <n>th row of a global table”. The debugger has installed its “handle-a-breakpoint” code there.
Similar, but with “source-level debugging” – present the pieces of your C source file that correspond to the compiled machine instructions.
The interpreter has “hooks” to do breakpoints.
Always there, even when not being used.
In some sense, an interpreter is a debugger, where you just usually don’t use any breakpoints or stepping.
Racket feels like an interpreted language:
There is a REPL (Read Eval Print Loop).
You can enter expressions to be evaluated, “live”.
REPL is really REEPL: Read Expand Eval Print Loop
Racket is a LISP. You can easily modify syntax – s-expressions – to produce other code.
Most common way to do this is “macros” – which are functions from syntax to syntax.
Your original program is expanded.
Expansion looks for macros and executes them to replace the original syntax with new syntax. Expansion continues until no macros remain.
Expansion can take awhile for a complicated “tower” of macros – seconds, maybe even tens of seconds.
As a result, Racket lets you “compile” source files to bytecode files. These are fully-expanded.
So: Read Expand Compile Eval Print Loop.
At runtime, bytecode is JIT-ed – Just In Time compiled to native machine code – before being evaluated.
Read Expand Compile JIT Eval Print Loop
A fully-expanded program is a small set of primitives.
No matter how complicated the original soure code, when fully-expanded it will use only this set of core forms.
/top-level-form/ = /general-top-level-form/ ⦚ (#%expression /expr/) ⦚ (module id module-path (#%plain-module-begin /module-level-form/ ...)) ⦚ (begin /top-level-form/ ...) ⦚ (begin-for-syntax /top-level-form/ ...) /module-level-form/ = /general-top-level-form/ ⦚ (#%provide raw-provide-spec ...) ⦚ (begin-for-syntax /module-level-form/ ...) ⦚ /submodule-form/ ⦚ (#%declare declaration-keyword ...) /submodule-form/ = (module id module-path (#%plain-module-begin /module-level-form/ ...)) ⦚ (module* id module-path (#%plain-module-begin /module-level-form/ ...)) ⦚ (module* id #f (#%plain-module-begin /module-level-form/ ...)) /general-top-level-form/ = /expr/ ⦚ (define-values (id ...) /expr/) ⦚ (define-syntaxes (id ...) /expr/) ⦚ (#%require raw-require-spec ...) /expr/ = id ⦚ (#%plain-lambda /formals/ /expr/ ...+) ⦚ (case-lambda (/formals/ /expr/ ...+) ...) ⦚ (if /expr/ /expr/ /expr/) ⦚ (begin /expr/ ...+) ⦚ (begin0 /expr/ /expr/ ...) ⦚ (let-values ([(id ...) /expr/] ...) /expr/ ...+) ⦚ (letrec-values ([(id ...) /expr/] ...) /expr/ ...+) ⦚ (set! id /expr/) ⦚ (quote datum) ⦚ (quote-syntax datum) ⦚ (quote-syntax datum #:local) ⦚ (with-continuation-mark /expr/ /expr/ /expr/) ⦚ (#%plain-app /expr/ ...+) ⦚ (#%top . id) ⦚ (#%variable-reference id) ⦚ (#%variable-reference (#%top . id)) ⦚ (#%variable-reference) /formals/ = (id ...) ⦚ (id ...+ . id) ⦚ id
And that’s where a step-debugger can step in!
Take the fully-expanded code for a program, and rewrite it to be a step-debuggable version of the program.
We want to “instrument” or “annotate” the code for step-debugging.
- At any “break-able” position – before or after an expression – decide whether to break there.
- If before an expression: Allow user to skip evaluating it. Instead substitute new values.
- If after an expression: Allow user to discard result. Instead substitute new values.
To do these three things, assume we have three functions – break?
, break-before
, and break-after
– that are somehow connected to the user interface for step-debugging.
Then our mission becomes: Rewrite the code to insert calls to these functions where appropriate.
(define (annotate-for-single-stepping stx break? break-before break-after)
(define (break-wrap debug-info annotated raw is-tail?)
(let* ([start (syntax-position raw)]
[end (+ start (syntax-span raw) -1)]
[break? (break? (syntax-source raw))])
(if is-tail?
#`(let-values ([(value-list) #f])
(if (#%plain-app #,break? #,start)
(set! value-list (#%plain-app
#,break-before
#,debug-info
(#%plain-app current-continuation-marks)))
(#%plain-app void))
(if (#%plain-app not value-list)
#,annotated
(#%plain-app plain-apply values value-list)))
#`(let-values ([(value-list) #f])
(if (#%plain-app #,break? #,start)
(set! value-list (#%plain-app
#,break-before
#,debug-info
(#%plain-app current-continuation-marks)))
(#%plain-app void))
(if (#%plain-app not value-list)
(#%plain-app
call-with-values
(#%plain-lambda () #,annotated)
(case-lambda
[(val) (if (#%plain-app #,break? #,end)
(#%plain-app
#,break-after
#,debug-info
(#%plain-app current-continuation-marks)
val)
val)]
[vals (if (#%plain-app
#,break? #,end)
(#%plain-app
plain-apply
#,break-after
#,debug-info
(#%plain-app current-continuation-marks)
vals)
(#%plain-app plain-apply values vals))]))
(if (#%plain-app #,break? #,end)
(#%plain-app
plain-apply #,break-after
#,debug-info
(#%plain-app current-continuation-marks)
value-list)
(#%plain-app plain-apply values value-list)))))))
(annotate-stx stx break-wrap))
Ignore:
- Racket allows functions to return mutiple values
- Racket does tail-call elimination
- Limiting ourselves to primitive forms
(define (annotate-for-step-debugging stx)
(define before-pos (syntax-position stx))
(define after-pos (+ before (syntax-span stx) -1))
#`(begin
(define before-value
(if (#,break? #,before-pos)
(#,break-before)
#false))
(if before-value
(if (#,break? #,after-pos)
(#,break-after before-value)
before-value)
(if (#,break? #,after-pos)
(#,break-after #,stx)
#,stx))))
Let’s see it in action with a simple example…
A “step-debugger running your program”
can simply be your program rewritten
to be “step-debug-able”
- racket-mode
- Racket
- fully-expanded programs grammar
- Greg Hendershott