Skip to content

Instantly share code, notes, and snippets.

@greghendershott
Last active September 12, 2018 12:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save greghendershott/05101f97fc6fed08bcd2da2225461255 to your computer and use it in GitHub Desktop.
Save greghendershott/05101f97fc6fed08bcd2da2225461255 to your computer and use it in GitHub Desktop.

-*- mode:org ; mode:visual-line -*-

Hack && Tell 6 Boston

What is a “debugger”?

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

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 (???)

What is a “program”?

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”.

Assembly language debugger

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.

C language deubgger

Similar, but with “source-level debugging” – present the pieces of your C source file that correspond to the compiled machine instructions.

Interpreted language

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 REPL

Racket feels like an interpreted language:

There is a REPL (Read Eval Print Loop).

You can enter expressions to be evaluated, “live”.

I lied: REEPL

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.

I lied: RECEPL

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.

The final lie: RECJEPL

At runtime, bytecode is JIT-ed – Just In Time compiled to native machine code – before being evaluated.

Read Expand Compile JIT Eval Print Loop

Fully Expanded Programs

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.

Implementation

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.

Actual code

(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))

Simplified code

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))))

Demo

Let’s see it in action with a simple example…

Conclusion

A “step-debugger running your program”

can simply be your program rewritten

to be “step-debug-able”

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment