A minimal debugger

Difference between version 19 and 20 - Previous - Next
[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...

<<categories>> Debugging | Arts and crafts of Tcl-Tk programming