Adding breakpoints to TkCon's internal debugger (idebug)

AMW 2011-11-27

I used the technique described in Steppin' out and A basic debugger for yet another approach to debugging.

Tkcon already contains an interactive debugger that can be invoked by instrumenting your script with calls to idebug break [1 ].

While this already is a very helpful tool, it requires modification of your existing code whenever you want to set a new breakpoint.

After sourcing the script shown below into Tkcon, you can enter the command bpadd procname at the Tkcon prompt to add a breakpoint at every call to procname without changing your code.

As soon as you run your code and procname is called, the interactive debugger is started.

Type ? to get idebug's menu, c to proceed until the next call to procname, or use any other existing command or feature of idebug.

Moreover, you can call s to 'step into' the intercepted proc, or r to return from it and see the results it produced.


#===============================================================================
#  This is an extension to the 'idebug' command in tkcon.tcl.
#  Please source this script into your tkcon session.
#===============================================================================

#-------------------------------------------------------------------------------
#  bpadd
#  add a breakpoint for calls to 'procname'
#-------------------------------------------------------------------------------
proc bpadd { procname } {
    global BP
    set BP($procname,step) 0
    trace add execution $procname enter     "BREAKPOINT $procname"
    trace add execution $procname enterstep "BREAKPOINT $procname"
    trace add execution $procname leave "BREAKPOINT $procname"
    idebug on
}

#-------------------------------------------------------------------------------
#  bplist
#  return a list of active breakpoints
#-------------------------------------------------------------------------------
proc bplist { } {
    global BP
    set procs ""
    foreach item [array names BP "*,step"] {
        regsub {,step$} $item "" procname
        lappend procs $procname
    }
    return [lsort $procs]
}

#-------------------------------------------------------------------------------
#  bpdel
#  delete breakpoint from calls to 'procname'
#  if procname is not given, it defaults to the 'current' breakpoint
#-------------------------------------------------------------------------------
proc bpdel { { procname "" } } {
    global BP
    if { $procname == "" } { set procname $BP(procname) }
    trace remove execution $procname enter     "BREAKPOINT $procname"
    trace remove execution $procname enterstep "BREAKPOINT $procname"
    trace remove execution $procname leave "BREAKPOINT $procname"
    catch { unset BP($procname,step) }
}

#-------------------------------------------------------------------------------
#  dbgStep
#  step into procname, i.e. call idebug at trace 'enterstep'
#-------------------------------------------------------------------------------
proc dbgStep { { procname "" } } {
    global BP IDEBUG
    if { $procname == "" } { set procname $BP(procname) }
    set BP($procname,step) 2
    # The following statement causes idebug to continue, but this requires
    # a small modification in tkcon.tcl (line 4287 in Tkcon 2.5):
    # in proc 'idebug' in the switch handling $opt="break", replace
    # 'while 1' for the input loop with 'while { $IDEBUG(debugging) }'
    set IDEBUG(debugging) 0
    return ""
}

#-------------------------------------------------------------------------------
#  dbgReturn
#  return from procname
#-------------------------------------------------------------------------------
proc dbgReturn { { procname "" } } {
    global BP IDEBUG
    if { $procname == "" } { set procname $BP(procname) }
    set BP($procname,step) 1
    set IDEBUG(debugging) 0
    return ""
}

#-------------------------------------------------------------------------------
#  shortcuts for use within 'idebug':
#  s - step into proc
#  r - return from proc
#-------------------------------------------------------------------------------
proc s { args } { dbgStep {*}$args }
proc r { args } { dbgReturn {*}$args }

#-------------------------------------------------------------------------------
#  dumpstr - dump str in the most readable way,
#  replacing non-printable characters with their hex-code written as "\xXX"
#-------------------------------------------------------------------------------
proc dumpstr { str } {
    set result ""
    while { $str != "" } {
        if { [regexp -indices {^[-+*/%<=>.,:;|~^°`´!$&@(){}\[\]#'"A-Z_a-z0-9]+} $str igood] } {
            # " readable characters, excluding backslash
            append result [string range $str 0 [lindex $igood 1]]
            set str [string range $str [lindex $igood 1]+1 end]
        }
        if { $str != "" } {
            set char [string index $str 0]
            set str  [string range $str 1 end]
            if { $char == "\\" } {
                append result {\\}
            } else {
                binary scan $char c hex
                append result [format "\\x%02X" [expr $hex & 0xff]]
            }
        }
    }
    append result $str
    return $result
}

#-------------------------------------------------------------------------------
#  BREAKPOINT handler
#-------------------------------------------------------------------------------
proc BREAKPOINT { procname cmd args } {
    global BP
    set BP(procname) $procname

    set op [lindex $args end]

    set dbg 1
    if { $op == "enter" } {
        puts stderr "# $cmd\nBREAKPOINT @ $procname $op"

    } elseif { $op == "enterstep"  && $BP($procname,step) >= 2 } {
        puts stderr "# $cmd\nBREAKPOINT @ $procname step"

    } elseif { $op == "leave"      && $BP($procname,step) >= 1 } {
        set code   [lindex $args 0]
        set result [lindex $args 1]
        puts stderr "# $cmd\nBREAKPOINT @ $procname leave (code $code, result \"[dumpstr $result]\")"

    } else {
        set dbg 0
    }

    if { $dbg } {
        set BP($procname,step) 0
        uplevel idebug break
    }
}

#===============================================================================
#  end of file
#===============================================================================

Of course, this could be added to tkcon.tcl in order to fully intergrate with idebug.