Search code examples
tcl

Get all parents from trace add execution command


My basic code :

proc A {} {B ; return 1}
proc B {} {C ; return 1}
proc C {} {return 1}

proc track {args} {
    # some informations : [info frame -2]
}
# trace '::C' procedure
trace add execution C leave track
# call proc '::A'
A

I tried to find some inspiration from this response.
My goal is to get all the parents of the procedure that is traced (here proc ::C) when this procedure is called.
Finally I would like that my track procedure to be able to return this :

::A call > ::B call > ::C

The return of [info frame -2] gives me interesting information:

type source line 7 file /Users/mkn/temp/trace_test.tcl cmd C proc ::B level 1

But I have a feeling this command only returns the first parent.


Solution

  • Depending on what exactly you mean, the basic information you are after comes from either the info level or info frame commands. They differ I their notion of what stack frames to count in ways that matter a lot when you are using uplevel; info frame "sees" it, but info level does not.

    Both commands give the number of stack frames (in their sense of it) when called withou arguments, and you can get the details of a stack frame by giving a number to identify it; info level returns the list of argument words at a particular level (the first word being the command name) and info frame returns a dictionary of goodies that provide more precise info. Level identifiers count from one end of the stack when positive and the other when negative. I can never remember which one zero is!

    Using info level

    For what you are trying to do, I'd guess that info level is a bit easier. Get the length of the list of procedure call frames, count up (or is it down?) over the frame numbers, mapping them to the list of words with info level, then picking the first word of each with lindex. Finally produce the result you want with a suitable join.

    proc track {call args} {
        set cmds {}
        for {set i 1} {$i < [info level]} {incr i} {
            lappend cmds [lindex [info level $i] 0]
        }
        # Because this is a leave trace we also need
        lappend cmds [lindex $call 0]
        puts [join $cmds " call > "]
    }
    

    The extra lappend is because the call to a leave trace happens when the call frame has just been unstacked. (info frame sees it differently.)

    The main thing I use info frame to retrieve is the source file and line number of a caller. That's ever so useful for logging and really isn't reported reliably by anything else. (Technically isn't here too, but dynamically-generated code usually doesn't need to log directly.)


    Using info frame

    The equivalent with info frame shows some key differences. (I'll not spend as much effort on the formatting.)

    proc track args {
        for {set i 1} {$i < [info frame]-1} {incr i} {
            lappend cmds [dict get [info frame $i] "cmd"]
        }
        puts $cmds
    }
    

    With the standard example from the question, that shows:

    A {B } {C }
    

    As you can see, unlike with info level the frame data is still stacked at the point when the trace is called.

    What's going on with those extra spaces? Well, that's actually showing sections of source code. If we alter the definition of A we can see it more clearly (from my interactive session):

    % # Allow the passing of arguments (that we ignore)
    % proc A args {B; return 1}
    % A 123
    {A 123} B {C }
    1
    % set x 123
    123
    % A $x
    {A $x} B {C }
    1
    

    It can therefore be noted that info frame is the only command in Tcl that can know whether an argument passed in came from a literal, a variable read or a command result. You are strongly recommended to not use this information for anything other than debugging! It's a principle of Tcl programming that a command should never care how its caller assembled the arguments to it.