A minimal debugger

Richard Suchenwirth 2001-01-16 -- Of course, Tcl's most minimal debugger is puts. But here is a cute little piece of code that offers some more debugging functionality (if you have stdin and stdout available - so not for wish on Windows):

 proc bp {{s {}}} {
        if ![info exists ::bp_skip] {
           set ::bp_skip [list]
        } elseif {[lsearch -exact $::bp_skip $s]>=0} return
        if [catch {info level -1} who] {set who ::}
        while 1 {
                puts -nonewline "$who/$s> "; flush stdout
                gets stdin line
                if {$line=="c"} {puts "continuing.."; break}
                if {$line=="i"} {set line "info locals"}
                catch {uplevel 1 $line} res
                puts $res
        }
 }

The idea is that you insert breakpoints, calls to bp, in critical code with an optional string argument (that may be used for distinguishing), like this:

 proc foo {args} {
        set x 1
        bp 1
        string toupper $args
 }
 foo bar and grill

When execution reaches bp, you get a prompt on stdout, giving the calling context and the bp string, like this:

 foo bar and grill/1> pwd
 /home/suchenwi/src/roi/test/mr
 foo bar and grill/1> i
 args x
 foo bar and grill/1> set args
 bar and grill
 foo bar and grill/1> set args lowercase
 lowercase
 foo bar and grill/1> c

on which you can issue any Tcl command (especially getting or setting variable values in the scope of foo), shorthand commands ("i" for "info locals"; add which others you need), and exit this micro-debugger with "c"(ontinue). Because you modified a local variable, foo's result will be

 LOWERCASE

To turn off all breakpoints for an application, just say (maybe from inside bp):

 proc bp args {}

You can disable single breakpoints labeled e.g. x with the command

 lappend ::bp_skip x

Stepping is made easy with the new execution traces from 8.4 - see Steppin' out, but in some situations a micro-debugger like this is good enough. See also debugging



ET Here's my take at this cool little debugger, to work with the windows console. It replaces the read from stdin to vwaiting on the global ___zzz___

See the do proc below for the commands added. The g command is a combined continue and dump locals on next breakpoint. It takes a counter N that will continue and skip N breakpoints.

There is now code following the bp proc that is optional which lets an <enter> alone repeat the last command from the history without saving the command repeatedly in the history. It is especially useful with the g command to watch the local variables change for a proc that sets a breakpoint, or in a loop. It should be fast enough to be able to hold down the enter key and watch the changes go by rapidly.

It does create one temporary variable ___var___ inside the proc that is unset at the end. This is if you use the d command (to dump the local variables).

proc do {args} {
    set ::___zzz___ $args
    if { [llength $::___zzz___ ] == 1 } {
        set ::___zzz___ [lindex $args 0 ]
    }
    #puts "args= |$args| [llength $::___zzz___ ]"
    #
    # do {command to run inside proc where the breakpoint sits} inside braces
    # d        dump local variables
    # c        continue from breakpoint
    # g        continue from breakpoint and at next break dump locals
    # g nnn    continue and go nnn times before next breakpoint, then dump locals
    # s        show stack
    #
    
}


proc d {} { ;# dump locals
    do d
}
proc g {args} { ;# dump locals and continue, can take a numerical skip counter e.g. g 20
    do g {*}$args
}
proc c {} { ;# continue
    do c
}
proc s {} { ;# stack dump
    do {puts {} ; for {set _n_ 10} {$_n_ >= 0} {incr _n_ -1} {catch {puts "$_n_ ) [string range [uplevel $_n_ info level 0] 0 80]" } } ; unset _n_}
}


proc bp {{s {}}} {
    console show
    console eval {focus -force .console} ;# this is the consoles text widget, focus in case it got lost (c would do that)
    incr ::___times___
    if { ! [info exists ::bp_counter] } {
        set ::bp_counter -1
    }
    set dowait 1
    if {[incr ::bp_counter -1] > 0} {
        return
    } else {
        if { $::bp_counter == 0 } {
            set ::___zzz___ d
            set dowait 0
        }
        set ::bp_counter -1
    }
    if {![info exists ::bp_skip]} {
        set ::bp_skip [list]
    } elseif {[lsearch -exact $::bp_skip $s]>=0} {
        return
    }
    
    if [catch {info level -1} who] {
        set who ::
    }
    set who [lindex $who 0 ]
    while 1 {
        if { $dowait } {
            puts -nonewline " $::___times___ \[$who\]/ "
            puts -nonewline stderr "$s> "; flush stdout
            update idletasks
            vwait ::___zzz___
        } else {
            set dowait 1 ;# for next time around
        }
        set line $::___zzz___
        set ::___zzz___ {}
        #puts "line= |$line| "
        set linedo $line
        #gets stdin line < now do this via a    set ___zzz___ {command}
        if {$line=="c"} { break}
        if {$line=="i"} {set linedo "info locals"}
        if {[lindex $line 0 ]=="d" || [lindex $line 0 ] == "g"} {
            set linedo {
                puts stderr "        proc = \[[lindex [info level 0] 0]\]   { [info args [lindex [info level 0] 0]] }"
                foreach ___var___ [lsort [info locals]] {
                    if { [array exist $___var___ ]} {
                        puts "[format %10s $___var___]() = ([string range [array names  $___var___] 0 80])"
                    } else {
                        puts "[format %12s $___var___] = |[string range [set $___var___] 0 80]|"
                        
                    }
                }
                unset  ___var___
            }
        }
        if { ![info exist linedo] } {
            set linedo $line
        }
        if {[lindex $line 0 ]=="g"} {
            set cnt [lindex $line 1 ]
            if { $cnt ne "" } {
                set ::bp_counter [expr {( $cnt  )}]
            } else {
                set ::bp_counter 1
            }
            break
        }
        #puts "linedo= |$linedo| "
        catch {uplevel 1 $linedo} res
        puts stderr $res
    }
}
#
#optional, to patch the console code to allow <return> alone to repeat last command
#
console eval {
    namespace eval tk { ; # replace this so we can capture a null command and repeat the last one
        proc ::tk::ConsoleInvoke {args} {
            set ranges [.console tag ranges input]
            set cmd ""
            if {[llength $ranges]} {
                set pos 0
                while {[lindex $ranges $pos] ne ""} {
                    set start [lindex $ranges $pos]
                    set end [lindex $ranges [incr pos]]
                    append cmd [.console get $start $end]
                    incr pos
                }
            }
            if {$cmd eq ""} {
                ConsolePrompt
            } elseif {[info complete $cmd]} {
                if { $cmd == "\n" } { #patch
                    set cmd_next [consoleinterp eval {history nextid}]
                    set cmd_event [consoleinterp eval "history event [expr {( $cmd_next - 1 )}]"]
                    if { $cmd_event != "" } {
                        set cmd $cmd_event
                        consoleinterp eval {namespace eval ::tcl {incr history(nextid) -1;incr history(oldest) -1}}  ;# don't store this one again into history
                    }
                }
                #end patch
                .console mark set output end
                .console tag delete input
                set result [consoleinterp record $cmd]
                if {$result ne ""} {
                    puts $result
                }
                ConsoleHistory reset
                ConsolePrompt
            } else {
                ConsolePrompt partial
            }
            .console yview -pickplace insert
        }
        
    }
}

# here's some test code with a 5 level deep set of calls to test the stack dump

button  .path1    -text "label1" -command {DoThis 500}
button  .path2    -text "label2" -command {DoThis 1000}
button  .path3    -text "label3" -command {DoThis 1000000}
button  .cons     -text "console" -command {console show}
pack  .path1 .path2 .path3  .cons -fill both -expand true

proc DoThis {args} {
    set last $args
    bp before-loop
    for {set n 0} {$n < $last} {incr n} {
        puts "args= |$args|   n= |$n| "
        #bp inloop
        DoThat $n
    }
    bp afterloop
}
proc DoThat {{myArg 100}} {
    DoAgain $myArg
    #bp indothat
}
proc DoAgain {myArg} {
    #bp DoAgainStart
    DoAgain1 $myArg
}
proc DoAgain1 {myArg} {
    #bp DoAgainStart1
    DoAgain2 $myArg
}
proc DoAgain2 {myArg} {
    #bp DoAgainStart2
    DoAgain3        $myArg
}
proc DoAgain3 {myArg} {
    bp DoAgain3

}



Bits and pieces.. Here's a minimal variable watcher, that logs every change to the specified variables to stdout:

 proc watch {args} {
     foreach arg $args {
        uplevel 1 "trace add variable $arg write {puts $arg:\[set $arg\] ;#}"
     }
 }

[Incidental remark: Expect builds in lots of interesting debugging capabilities.]


Tkcon has a couple of debugging features like those described above. idebug is like bp above but with a few more bells and whistles, and observe is like watch above. [kpv]


AM It is very easy to extend this minimal debugger to become a mini debugger. To keep the intrusion as small as possible, just handle all debugging commands that you want to implement in a switch statement (rather than the cascading "if") for readability. This way you introduce but a single new command, bp, instead of a handful (but it may be useful to define a few, like watch, as a procedure, so that you can log the variables automatically.

One problem remains (one that I would like to solve or see solved!): how to get this to work under Windows - perhaps a solution via a pipe? The reason I am concerned is that I use (want to use) Tcl in an "embedded" mode. I can not use Tk in there, so a solution via Tkcon or console show becomes difficult... - RS: What is the problem? The little one above works only with stdin and stdout, no Tk required; and I've made good use of it debugging our app where a Tcl interpreter is embedded.


See also An error experiment with an even minimaller debugger...