Review requested: Binding.call_stack(), Binding.of_caller()-style

Discussion in 'Ruby' started by Dumaiu, Mar 4, 2006.

  1. Dumaiu

    Dumaiu Guest

    Hi, all--

    I'd like to submit for evaluation an elaboration I've made on the
    famous 'Binding.of_caller()' of Florian Groß. I have tried to follow
    the the description of the interface for Binding.call_stack() suggested
    in ruby-talk 12097, "RCR: replacing 'caller'", although in standard
    Ruby instead of an extension.
    The technique of using set_trace_func() to capture a Binding one
    stack frame up has been around at least since 2001 [1]. Here I raise
    an exception and ride it out to main scope, catching Bindings as the
    stack unrolls. File and line info is taken from caller() as usual, and
    the input from the two traces is knitted together.

    #<call_stack.rb>
    =begin
    Binding::call_stack() returns a CallStack object--an Array
    subclass--containing one StackFrame object per calling frame. Unless
    specified otherwise, the last StackFrame will contain information from
    the outermost scope, the 'main' quasi-object. A StackFrame, like a
    Struct, is a bundle of attributes, as follows:

    'object': Name of module|class in which the calling function is
    defined. Will be 'main' if call_stack() is invoked at global scope.
    'method': Name of method whose calling scope we currently inhabit.
    Will be nil at global scope.
    'binding': Binding within calling method's scope. This is the really
    useful one, carrying on Binding.of_caller()'s performance.
    'file': Current file.
    'line': Number of line at which current method was called, as
    returned by Kernel::caller().

    As far as I have understood the logic behind caller(), I have tried to
    integrate the Binding retrieval smoothly thereinto. Here are some
    examples:

    # <stacktest.rb>
    require 'call_stack'

    puts Binding::call_stack.first.to_a # Retrieve StackFrame and convert
    to array for output
    # </stacktest.rb>

    produces:

    main
    nil
    #<Binding:0xb16fa4f00l>
    stacktest.rb
    4

    Not interesting. But given this

    # <stacktest.rb>
    require 'call_stack'

    main_local = 'main scope'

    class C
    def first()
    first_local = 'in C.first()'
    return second
    end

    def second()
    second_local = 'in C.second()'
    return Binding::call_stack # <---- Nested call
    end
    end# C

    # Print CallStack array:
    C.new.first().each { |frame|
    # Dump current frame:
    puts "Class: '%s'" % [frame.object]
    puts "Method name: '%s'" % [frame.method]
    puts "Filename: '%s'" % [frame.file]
    puts "Line no.: %d" % [frame.line]
    puts "Locals in binding: #{eval 'local_variables()',
    frame.binding}\n\n"
    }# each
    # </stacktest.rb>

    we see

    Class: 'C'
    Method name: 'second'
    Filename: 'stacktest.rb'
    Line no.: 18
    Locals in binding: second_local

    Class: 'C'
    Method name: 'first'
    Filename: 'stacktest.rb'
    Line no.: 13
    Locals in binding: first_local

    Class: 'main'
    Method name: ''
    Filename: 'stacktest.rb'
    Line no.: 22
    Locals in binding: main_local

    The first frame holds the environment in which call_stack() was itself
    called, that is, the method C#second(). The second frame holds *it's*
    calling environment, C#first(), and the third frame, global binding.
    The eval()'d calls to Kernel::local_variables() suggest how these
    captured bindings can be used to mess with other people's stuff.
    Three configuration attributes control the classes used internally:

    * Binding::CallStack::frame_class : The class held here is instantiated
    into containers for the data returned by call_stack(). Default value
    is Binding::StackFrame.
    * Binding::CallStack::tracing_exception : The exception thrown to
    unroll the stack. Because it arrests itself, this is one exception we
    *don't* want to be caught, to which end its type is generated from the
    system clock when the file is loaded. But you can change it.
    * Binding::stack_class : The class instantiated for return by
    call_stack(). In this case, it must quack like an Array with two extra
    methods--call(), invoked each time a frame is added; and ret(), invoked
    just before the object is returned.

    =end

    # BINDING
    class Binding
    require 'test/unit/assertions'
    class << self
    include Test::Unit::Assertions # for Binding::call_stack
    end

    # *************** STACKFRAME ***************
    class StackFrame
    include Test::Unit::Assertions

    # TODO: Make read-only:
    attr_accessor :eek:bject
    attr_accessor :method
    attr_accessor :binding
    attr_accessor :file
    attr_accessor :line

    # to_a(): Return data as new array.
    def to_a()
    return [ self.object,
    self.method,
    self.binding,
    self.file,
    self.line
    ]
    end# to_a()

    # to_s(): Return join()ed string.
    def to_s()
    return to_a().to_s()
    end# to_s()

    # inspect(): Return as array of values.
    def inspect()
    return to_a().inspect()
    end# inspect()

    # to_h(): Return in key/value form. Note: A pair will only exist
    if accessor assignment has been used.
    def to_h()
    h = Hash::new
    var = nil # temp
    instance_variables().each { |var|
    var =~ /^@ (\w+) $/x
    assert( $1 )
    h[$1] = instance_variable_get(var)
    }# each

    return h
    end# to_h()
    end# StackFrame

    # *************** CALLSTACK ***************
    # Actually an Array, not a Stack proper.
    class CallStack < Array
    include Test::Unit::Assertions
    require 'date'

    @version = '0.0.1'
    class << self
    attr_reader :version
    end


    # Class instance vars:
    @frame_class = Binding::StackFrame
    @tracing_exception = "xCallStack: #{DateTime.now.to_s}".intern()
    class << self
    attr_accessor :frame_class
    attr_accessor :tracing_exception
    end

    # Ctor: call_stack() passes its binding hither, in case we need to
    read locals therefrom (a utility device). Read-only!
    # Note: A null value for call_stack_binding will yield an empty
    object.
    def initialize( call_stack_binding = nil )
    if( call_stack_binding )
    @call_stack_binding = call_stack_binding # Store for use in
    call().

    push StackFrame::new() # Disposable: eliminated in ret().
    end# if
    # else empty
    end# ctor()

    # call(): Repeatedly used by call_stack() to add frames to the
    array (hence the name).
    # [ class, method, binding, file, line ]
    def call( trace_event, trace_file, trace_line, trace_method,
    trace_bind, trace_object )
    assert( eval( "defined? aCallers", @call_stack_binding ) ==
    'local-variable' ) # Ensure we have the proper name for locvar in
    call_stack().
    aCallers = eval( "aCallers", @call_stack_binding ) # temp to
    avoid repeat eval() calls

    push StackFrame::new()
    # The nature of set_trace_func() is to return the class and
    method name for the *previous* stack frame.
    at(-2).object = trace_object
    at(-2).method = trace_method

    last.binding = trace_bind
    aCallers.first =~ /^ ( [\w\.]+ ) : ( \d+ ) \D*/x
    assert( $1 && $2 )
    last.file = $1
    last.line = $2

    return self
    end# call()

    # ret(): Called after all iterations are finished, just before the
    CallStack is passed back to the client.
    def ret()
    shift() # Remove placeholder first element.
    assert( length > 0 )

    assert( eval( "defined? nCount", @call_stack_binding ) ==
    'local-variable' ) # Ensure we have the proper name for locvar in
    call_stack().

    # If the final frame isn't at main scope, depending on the
    arguments to call_stack(), it's also a placeholder and must be removed:
    if( eval( "nCount", @call_stack_binding ) )
    pop()
    else
    # Flesh out final element:
    last.object = 'main'
    last.method = nil
    end# if

    return self
    end# ret()
    end# CallStack

    # *************** CALL_STACK ***************
    # call_stack(): Retrieve CallStack object (array of StackFrame
    objects).
    def Binding::call_stack( nSkipFrames = 0, nCount = nil )
    # Validate vars:
    [nSkipFrames, nCount].each { |var|
    if( var && (!var.is_a?(Integer) || (var < 0 )) ) then raise(
    ArgumentError, "Optional argument to call_stack() should be a
    nonnegative Integer." ); end
    }# each

    # Store temporarily to obviate reinvocation. Needed below, and
    also by the current implementation of class CallStack, so I opted to
    store it here and pass the CallStack ctor this, call_stack()'s,
    binding, to allow for greater flexibility if CallStack is extended.
    aCallers = caller()

    # If at toplevel or nSkipFrames is greater than the number of
    frames available, /or/ the client explicitly requests zero frames, exit
    early:
    if( (aCallers.length-nSkipFrames <= 0) || (nCount == 0) ) then
    return Binding::stack_class::new(); end

    # Trim the first entry, the client function, which we don't want to
    include; then omit the next nSkipFrames entries.

    # If nCount sufficiently large, no reason to use it at all:
    if( nCount && ( nSkipFrames+nCount >= aCallers.length) ) then
    nCount = nil; end

    # Trim end if nCount provided:
    if( nCount )
    assert( nCount < aCallers.length )
    nCount += nSkipFrames+1 # If nCount must be used in its "proper"
    function, we have to pad out the number of trace calls to one beyond
    the specified, in order to receive object and method info (see
    CallStack::call(), above).
    aCallers.slice!(nCount .. -1)
    end# end

    aCallStack = Binding::stack_class::new( binding() ) # Pass current
    Binding, giving CallStack access to locals.
    callcc { |cc|
    # Hope springs eternal(!):
    #old_tracefunc = get_trace_func()
    set_trace_func(
    proc {
    # event, file, line, id, bind, classname
    |event, *remaining_args|

    if( event == 'return' )
    # If frame capture yet enabled:
    if( nSkipFrames == 0 )
    # capture current:
    aCallStack.call( event, *remaining_args )
    else # skip to next
    nSkipFrames -= 1
    end# if

    aCallers.shift()

    # If at toplevel, end ride:
    if( aCallers.length == 0 )
    #set_trace_func( get_trace_func() )
    set_trace_func( nil )

    # -----> Stack unrolling finishes here:
    cc.call()
    end# if
    end# if
    }# proc
    )# set_trace_func

    # Stack unrolling begins here: ----->
    throw( Binding::CallStack::tracing_exception )
    }# callcc

    assert(nSkipFrames)

    # Resume execution in client func:
    aCallStack.ret()
    return( aCallStack )
    end# call_stack()

    # Class instance var:
    @stack_class = Binding::CallStack
    class << self
    attr_accessor :stack_class
    end
    end# Binding

    # </call_stack.rb>

    I would appreciate your input with regard to the following:
    * The three class attributes mentioned above (and the version member)
    allow noninvasive tweaking, but I'm not sure whether I should be using
    class variables or class instance vars for them.
    * I noticed that Herr Groß renders his script thread critical(). I
    know not enough about thread safety to incorporate this without dumbly
    parroting him.
    * Use of set_trace_func() breaks compatibility with the debugger, and
    I haven't had much luck with irb, either. Under what other
    circumstances do Binding.of_caller() and relatives not work well?

    Also, my rdoc skills are rudimentary and there's a sort of half-assed
    Hungarian notation going on, because I haven't really worked out a
    style for myself. :)-P
    Finally: I've only been in Ruby for a little over a month, so if it
    looks somewhere like I don't know what I'm doing, that's probably the
    case. :)
    If I have wasted your time, please don't look back.


    Yours,

    Jonathan J-S

    Off to bed.

    * [1]: I've started thinking of this as the 'red carpet idiom.' Any
    takers?
     
    Dumaiu, Mar 4, 2006
    #1
    1. Advertising

  2. Wow, impressive. I can't comment very much on the implementation, I'm
    still trying to wrap my head around what you've done. Would you mind
    writing a little bit about how you are using continuations to walk the
    stack in the Binding::call_stack method? How does that interact with
    set_trace_func and the throw at the bottom of the callcc block? (e.g.=20
    throw( Binding::CallStack::tracing_exception ))

    Looks pretty cool to me!


    On 3/4/06, Dumaiu <> wrote:
    > Hi, all--
    >
    > I'd like to submit for evaluation an elaboration I've made on the
    > famous 'Binding.of_caller()' of Florian Gro=DF. I have tried to follow
    > the the description of the interface for Binding.call_stack() suggested
    > in ruby-talk 12097, "RCR: replacing 'caller'", although in standard
    > Ruby instead of an extension.
    > The technique of using set_trace_func() to capture a Binding one
    > stack frame up has been around at least since 2001 [1]. Here I raise
    > an exception and ride it out to main scope, catching Bindings as the
    > stack unrolls. File and line info is taken from caller() as usual, and
    > the input from the two traces is knitted together.
    >
    > #<call_stack.rb>
    > =3Dbegin
    > Binding::call_stack() returns a CallStack object--an Array
    > subclass--containing one StackFrame object per calling frame. Unless
    > specified otherwise, the last StackFrame will contain information from
    > the outermost scope, the 'main' quasi-object. A StackFrame, like a
    > Struct, is a bundle of attributes, as follows:
    >
    > 'object': Name of module|class in which the calling function is
    > defined. Will be 'main' if call_stack() is invoked at global scope.
    > 'method': Name of method whose calling scope we currently inhabit.
    > Will be nil at global scope.
    > 'binding': Binding within calling method's scope. This is the really
    > useful one, carrying on Binding.of_caller()'s performance.
    > 'file': Current file.
    > 'line': Number of line at which current method was called, as
    > returned by Kernel::caller().
    >
    > As far as I have understood the logic behind caller(), I have tried to
    > integrate the Binding retrieval smoothly thereinto. Here are some
    > examples:
    >
    > # <stacktest.rb>
    > require 'call_stack'
    >
    > puts Binding::call_stack.first.to_a # Retrieve StackFrame and convert
    > to array for output
    > # </stacktest.rb>
    >
    > produces:
    >
    > main
    > nil
    > #<Binding:0xb16fa4f00l>
    > stacktest.rb
    > 4
    >
    > Not interesting. But given this
    >
    > # <stacktest.rb>
    > require 'call_stack'
    >
    > main_local =3D 'main scope'
    >
    > class C
    > def first()
    > first_local =3D 'in C.first()'
    > return second
    > end
    >
    > def second()
    > second_local =3D 'in C.second()'
    > return Binding::call_stack # <---- Nested call
    > end
    > end# C
    >
    > # Print CallStack array:
    > C.new.first().each { |frame|
    > # Dump current frame:
    > puts "Class: '%s'" % [frame.object]
    > puts "Method name: '%s'" % [frame.method]
    > puts "Filename: '%s'" % [frame.file]
    > puts "Line no.: %d" % [frame.line]
    > puts "Locals in binding: #{eval 'local_variables()',
    > frame.binding}\n\n"
    > }# each
    > # </stacktest.rb>
    >
    > we see
    >
    > Class: 'C'
    > Method name: 'second'
    > Filename: 'stacktest.rb'
    > Line no.: 18
    > Locals in binding: second_local
    >
    > Class: 'C'
    > Method name: 'first'
    > Filename: 'stacktest.rb'
    > Line no.: 13
    > Locals in binding: first_local
    >
    > Class: 'main'
    > Method name: ''
    > Filename: 'stacktest.rb'
    > Line no.: 22
    > Locals in binding: main_local
    >
    > The first frame holds the environment in which call_stack() was itself
    > called, that is, the method C#second(). The second frame holds *it's*
    > calling environment, C#first(), and the third frame, global binding.
    > The eval()'d calls to Kernel::local_variables() suggest how these
    > captured bindings can be used to mess with other people's stuff.
    > Three configuration attributes control the classes used internally:
    >
    > * Binding::CallStack::frame_class : The class held here is instantiated
    > into containers for the data returned by call_stack(). Default value
    > is Binding::StackFrame.
    > * Binding::CallStack::tracing_exception : The exception thrown to
    > unroll the stack. Because it arrests itself, this is one exception we
    > *don't* want to be caught, to which end its type is generated from the
    > system clock when the file is loaded. But you can change it.
    > * Binding::stack_class : The class instantiated for return by
    > call_stack(). In this case, it must quack like an Array with two extra
    > methods--call(), invoked each time a frame is added; and ret(), invoked
    > just before the object is returned.
    >
    > =3Dend
    >
    > # BINDING
    > class Binding
    > require 'test/unit/assertions'
    > class << self
    > include Test::Unit::Assertions # for Binding::call_stack
    > end
    >
    > # *************** STACKFRAME ***************
    > class StackFrame
    > include Test::Unit::Assertions
    >
    > # TODO: Make read-only:
    > attr_accessor :eek:bject
    > attr_accessor :method
    > attr_accessor :binding
    > attr_accessor :file
    > attr_accessor :line
    >
    > # to_a(): Return data as new array.
    > def to_a()
    > return [ self.object,
    > self.method,
    > self.binding,
    > self.file,
    > self.line
    > ]
    > end# to_a()
    >
    > # to_s(): Return join()ed string.
    > def to_s()
    > return to_a().to_s()
    > end# to_s()
    >
    > # inspect(): Return as array of values.
    > def inspect()
    > return to_a().inspect()
    > end# inspect()
    >
    > # to_h(): Return in key/value form. Note: A pair will only exist
    > if accessor assignment has been used.
    > def to_h()
    > h =3D Hash::new
    > var =3D nil # temp
    > instance_variables().each { |var|
    > var =3D~ /^@ (\w+) $/x
    > assert( $1 )
    > h[$1] =3D instance_variable_get(var)
    > }# each
    >
    > return h
    > end# to_h()
    > end# StackFrame
    >
    > # *************** CALLSTACK ***************
    > # Actually an Array, not a Stack proper.
    > class CallStack < Array
    > include Test::Unit::Assertions
    > require 'date'
    >
    > @version =3D '0.0.1'
    > class << self
    > attr_reader :version
    > end
    >
    >
    > # Class instance vars:
    > @frame_class =3D Binding::StackFrame
    > @tracing_exception =3D "xCallStack: #{DateTime.now.to_s}".intern()
    > class << self
    > attr_accessor :frame_class
    > attr_accessor :tracing_exception
    > end
    >
    > # Ctor: call_stack() passes its binding hither, in case we need to
    > read locals therefrom (a utility device). Read-only!
    > # Note: A null value for call_stack_binding will yield an empty
    > object.
    > def initialize( call_stack_binding =3D nil )
    > if( call_stack_binding )
    > @call_stack_binding =3D call_stack_binding # Store for use in
    > call().
    >
    > push StackFrame::new() # Disposable: eliminated in ret().
    > end# if
    > # else empty
    > end# ctor()
    >
    > # call(): Repeatedly used by call_stack() to add frames to the
    > array (hence the name).
    > # [ class, method, binding, file, line ]
    > def call( trace_event, trace_file, trace_line, trace_method,
    > trace_bind, trace_object )
    > assert( eval( "defined? aCallers", @call_stack_binding ) =3D=3D
    > 'local-variable' ) # Ensure we have the proper name for locvar in
    > call_stack().
    > aCallers =3D eval( "aCallers", @call_stack_binding ) # temp to
    > avoid repeat eval() calls
    >
    > push StackFrame::new()
    > # The nature of set_trace_func() is to return the class and
    > method name for the *previous* stack frame.
    > at(-2).object =3D trace_object
    > at(-2).method =3D trace_method
    >
    > last.binding =3D trace_bind
    > aCallers.first =3D~ /^ ( [\w\.]+ ) : ( \d+ ) \D*/x
    > assert( $1 && $2 )
    > last.file =3D $1
    > last.line =3D $2
    >
    > return self
    > end# call()
    >
    > # ret(): Called after all iterations are finished, just before the
    > CallStack is passed back to the client.
    > def ret()
    > shift() # Remove placeholder first element.
    > assert( length > 0 )
    >
    > assert( eval( "defined? nCount", @call_stack_binding ) =3D=3D
    > 'local-variable' ) # Ensure we have the proper name for locvar in
    > call_stack().
    >
    > # If the final frame isn't at main scope, depending on the
    > arguments to call_stack(), it's also a placeholder and must be removed:
    > if( eval( "nCount", @call_stack_binding ) )
    > pop()
    > else
    > # Flesh out final element:
    > last.object =3D 'main'
    > last.method =3D nil
    > end# if
    >
    > return self
    > end# ret()
    > end# CallStack
    >
    > # *************** CALL_STACK ***************
    > # call_stack(): Retrieve CallStack object (array of StackFrame
    > objects).
    > def Binding::call_stack( nSkipFrames =3D 0, nCount =3D nil )
    > # Validate vars:
    > [nSkipFrames, nCount].each { |var|
    > if( var && (!var.is_a?(Integer) || (var < 0 )) ) then raise(
    > ArgumentError, "Optional argument to call_stack() should be a
    > nonnegative Integer." ); end
    > }# each
    >
    > # Store temporarily to obviate reinvocation. Needed below, and
    > also by the current implementation of class CallStack, so I opted to
    > store it here and pass the CallStack ctor this, call_stack()'s,
    > binding, to allow for greater flexibility if CallStack is extended.
    > aCallers =3D caller()
    >
    > # If at toplevel or nSkipFrames is greater than the number of
    > frames available, /or/ the client explicitly requests zero frames, exit
    > early:
    > if( (aCallers.length-nSkipFrames <=3D 0) || (nCount =3D=3D 0) ) then
    > return Binding::stack_class::new(); end
    >
    > # Trim the first entry, the client function, which we don't want to
    > include; then omit the next nSkipFrames entries.
    >
    > # If nCount sufficiently large, no reason to use it at all:
    > if( nCount && ( nSkipFrames+nCount >=3D aCallers.length) ) then
    > nCount =3D nil; end
    >
    > # Trim end if nCount provided:
    > if( nCount )
    > assert( nCount < aCallers.length )
    > nCount +=3D nSkipFrames+1 # If nCount must be used in its "proper"
    > function, we have to pad out the number of trace calls to one beyond
    > the specified, in order to receive object and method info (see
    > CallStack::call(), above).
    > aCallers.slice!(nCount .. -1)
    > end# end
    >
    > aCallStack =3D Binding::stack_class::new( binding() ) # Pass current
    > Binding, giving CallStack access to locals.
    > callcc { |cc|
    > # Hope springs eternal(!):
    > #old_tracefunc =3D get_trace_func()
    > set_trace_func(
    > proc {
    > # event, file, line, id, bind, classname
    > |event, *remaining_args|
    >
    > if( event =3D=3D 'return' )
    > # If frame capture yet enabled:
    > if( nSkipFrames =3D=3D 0 )
    > # capture current:
    > aCallStack.call( event, *remaining_args )
    > else # skip to next
    > nSkipFrames -=3D 1
    > end# if
    >
    > aCallers.shift()
    >
    > # If at toplevel, end ride:
    > if( aCallers.length =3D=3D 0 )
    > #set_trace_func( get_trace_func() )
    > set_trace_func( nil )
    >
    > # -----> Stack unrolling finishes here:
    > cc.call()
    > end# if
    > end# if
    > }# proc
    > )# set_trace_func
    >
    > # Stack unrolling begins here: ----->
    > throw( Binding::CallStack::tracing_exception )
    > }# callcc
    >
    > assert(nSkipFrames)
    >
    > # Resume execution in client func:
    > aCallStack.ret()
    > return( aCallStack )
    > end# call_stack()
    >
    > # Class instance var:
    > @stack_class =3D Binding::CallStack
    > class << self
    > attr_accessor :stack_class
    > end
    > end# Binding
    >
    > # </call_stack.rb>
    >
    > I would appreciate your input with regard to the following:
    > * The three class attributes mentioned above (and the version member)
    > allow noninvasive tweaking, but I'm not sure whether I should be using
    > class variables or class instance vars for them.
    > * I noticed that Herr Gro=DF renders his script thread critical(). I
    > know not enough about thread safety to incorporate this without dumbly
    > parroting him.
    > * Use of set_trace_func() breaks compatibility with the debugger, and
    > I haven't had much luck with irb, either. Under what other
    > circumstances do Binding.of_caller() and relatives not work well?
    >
    > Also, my rdoc skills are rudimentary and there's a sort of half-assed
    > Hungarian notation going on, because I haven't really worked out a
    > style for myself. :)-P
    > Finally: I've only been in Ruby for a little over a month, so if it
    > looks somewhere like I don't know what I'm doing, that's probably the
    > case. :)
    > If I have wasted your time, please don't look back.
    >
    >
    > Yours,
    >
    > Jonathan J-S
    >
    > Off to bed.
    >
    > * [1]: I've started thinking of this as the 'red carpet idiom.' Any
    > takers?
    >
    >
    >
     
    Justin Bailey, Mar 4, 2006
    #2
    1. Advertising

  3. Dumaiu

    Dumaiu Guest

    Hiya!

    Sure, I'll talk some more. It took *me* a while to wrap my head
    around Florian Gross's stuff. I just didn't want to look like I was
    trying to talk down to anyone.
    As we know, Kernel::caller() returns an array of strings, and is
    pretty frickin' useless by itself. A lot of people, independent of one
    another, have wanted a way to write library methods that can
    "magically" access the scope in which they were invoked. Gross wrote a
    library to do this, binding_of_caller.rb, which has been floating
    around this newsgroup for a while; and if you haven't seen it yet, you
    shouldn't have trouble finding it. The only way to generate Binding
    objects automatically is set_trace_func(), so you'll see it in every
    attempt of this sort. You stick in a trace func that catches the
    bindings from the 'return' events. Gross's clever solution was to use
    a continuation to mark a location, take a detour to collect Bindings,
    and then resume. Scenario: you write a library function requiring
    access to its caller's binding. Your function calls his
    Binding::eek:f_caller(), puts a block into set_trace_func(), creates a
    continuation, and allows itself to return. You have now moved back a
    stack frame into your original function. The tracer notes this. Your
    function then *also* returns immediately (enforced by
    binding_of_caller.rb), and with the second 'return' event you get the
    binding you're after. Once the trace func sees this, it uses the
    cached continuation to dive back into normal execution.
    The necessity of Binding::eek:f_caller() being a tail call bothered
    me. Instead of

    def library_func()
    bind = Binding::eek:f_caller
    do_something( bind )
    end

    you must split your function into an outer and an inner component:

    def library_func() # Header
    Binding::eek:f_caller { |bind| library_func_impl(bind) } #
    Continuation is only available as a block parameter.
    # Nothing allowed here
    end

    def library_func_impl(bind) # Body
    do_something( bind )
    end

    That block is required. Not concealed enough! The only novelty in my
    code is the use of an escape continuation, i.e., an exception, which
    eliminates the tail-call thing: set_trace_func() sees nothing but a
    pile of 'return' events until it reaches the top (bottom? top?) of the
    stack, and then--*just* before the uncaught exception crashes the
    program--the stored continuation is called. Roughly equivalent to the
    previous example is

    def library_func()
    bind = Binding::call_stack(1,1).first.binding
    do_something( bind )
    end

    call_stack() calls callcc(). callcc() calls set_trace_func() and
    throws an exception. The exception unrolls the stack, triggering the
    tracer automatically. The tracer returns flow to call_stack() and
    then unsets itself. The real stuff is all in Binding::call_stack(),
    the rest just 'limbs and outward flourishes.' I don't know about you,
    but I had to work very hard to understand Kernel::callcc(). It's
    spaghetti syntax, to be sure.
    F.G.'s design is faster and optimal if you only need to regress one
    frame. Once I realized I could get the whole stack, it seemed silly to
    return an array of just Bindings only, so I made it more like I wished
    Kernel::caller() had worked to begin with. The implementation is a
    byzantine shot at balancing extensibility and efficiency. In
    particular, the capability of passing start- and endpoints as
    arguments, the last feature added and the least well tested,
    complicated things.

    Take care,

    -Jonathan
     
    Dumaiu, Mar 4, 2006
    #3
  4. Dumaiu

    Dumaiu Guest

    Update. So I learn that using an exception as the unrolling
    mechanism will trip any ensure() blocks it passes through: each will
    get executed twice, once during and once after, and the client won't
    know why. Included is the patch--the entire file without the docs, for
    simplicity's sake. A row of asterisks marks the only addition.

    #<call_stack.rb>

    # Pop open for a bit:
    class Binding
    require 'test/unit/assertions'
    class << self
    include Test::Unit::Assertions # for Binding::call_stack
    end

    # *************** STACKFRAME ***************
    class StackFrame
    include Test::Unit::Assertions

    # TODO: Make read-only:
    attr_accessor :eek:bject
    attr_accessor :method
    attr_accessor :binding
    attr_accessor :file
    attr_accessor :line

    # to_a(): Return data as new array.
    def to_a()
    return [ self.object,
    self.method,
    self.binding,
    self.file,
    self.line
    ]
    end# to_a()

    # to_s(): Return join()ed string.
    def to_s()
    return to_a().to_s()
    end# to_s()

    # inspect(): Return as array of values.
    def inspect()
    return to_a().inspect()
    end# inspect()

    # to_h(): Return in key/value form. Note: A pair will only exist
    if accessor assignment has been used.
    def to_h()
    h = Hash::new
    var = nil # temp
    instance_variables().each { |var|
    var =~ /^@ (\w+) $/x
    assert( $1 )
    h[$1] = instance_variable_get(var)
    }# each

    return h
    end# to_h()
    end# StackFrame

    # *************** CALLSTACK ***************
    # Actually an Array, not a Stack proper.
    class CallStack < Array
    include Test::Unit::Assertions
    require 'date'

    @version = '0.1.1'
    class << self
    attr_reader :version
    end


    # Class instance vars:
    @frame_class = Binding::StackFrame
    @tracing_exception = "xCallStack: #{DateTime.now.to_s}".intern()
    class << self
    attr_accessor :frame_class
    attr_accessor :tracing_exception
    end

    # Ctor: call_stack() passes its binding hither, in case we need to
    read locals therefrom (a utility device). Read-only!
    # Note: A null value for call_stack_binding will yield an empty
    object.
    def initialize( call_stack_binding = nil )
    if( call_stack_binding )
    @call_stack_binding = call_stack_binding # Store for use in
    call().

    push StackFrame::new() # Disposable: eliminated in ret().
    end# if
    # else empty
    end# ctor()

    # call(): Repeatedly used by call_stack() to add frames to the
    array (hence the name).
    # [ class, method, binding, file, line ]
    def call( trace_event, trace_file, trace_line, trace_method,
    trace_bind, trace_object )
    assert( eval( "defined? aCallers", @call_stack_binding ) ==
    'local-variable' ) # Ensure we have the proper name for locvar in
    call_stack().
    aCallers = eval( "aCallers", @call_stack_binding ) # temp to
    avoid repeat eval() calls

    push StackFrame::new()
    # The nature of set_trace_func() is to return the class and
    method name for the *previous* stack frame.
    at(-2).object = trace_object
    at(-2).method = trace_method

    last.binding = trace_bind
    aCallers.first =~ /^ ( [\w\.]+ ) : ( \d+ ) \D*/x
    assert( $1 && $2 )
    last.file = $1
    last.line = $2

    return self
    end# call()

    # ret(): Called after all iterations are finished, just before the
    CallStack is passed back to the client.
    def ret()
    shift() # Remove placeholder first element.
    assert( length > 0 )

    assert( eval( "defined? nCount", @call_stack_binding ) ==
    'local-variable' ) # Ensure we have the proper name for locvar in
    call_stack().

    # If the final frame isn't at main scope, depending on the
    arguments to call_stack(), it's also a placeholder and must be removed:
    if( eval( "nCount", @call_stack_binding ) )
    pop()
    else
    # Flesh out final element:
    last.object = 'main'
    last.method = nil
    end# if

    return self
    end# ret()
    end# CallStack

    # *************** CALL_STACK ***************
    # call_stack(): Retrieve CallStack object (array of StackFrame
    objects).
    def Binding::call_stack( nSkipFrames = 0, nCount = nil )
    # Validate vars:
    [nSkipFrames, nCount].each { |var|
    if( var && (!var.is_a?(Integer) || (var < 0 )) ) then raise(
    ArgumentError, "Optional argument to call_stack() should be a
    nonnegative Integer." ); end
    }# each

    # Store temporarily to obviate reinvocation. Needed below, and
    also by the current implementation of class CallStack, so I opted to
    store it here and pass the CallStack ctor this, call_stack()'s,
    binding, to allow for greater flexibility if CallStack is extended.
    aCallers = caller()

    # If at toplevel or nSkipFrames is greater than the number of
    frames available, /or/ the client explicitly requests zero frames, exit
    early:
    if( (aCallers.length-nSkipFrames <= 0) || (nCount == 0) ) then
    return Binding::stack_class::new(); end

    # Trim the first entry, the client function, which we don't want to
    include; then omit the next nSkipFrames entries.
    #aCallers.slice!(0, nSkipFrames) FIXME: Don't let this creep back
    in!

    # If nCount sufficiently large, no reason to use it at all:
    if( nCount && ( nSkipFrames+nCount >= aCallers.length) ) then
    nCount = nil; end

    # Trim end if nCount provided:
    if( nCount )
    assert( nCount < aCallers.length )
    nCount += nSkipFrames+1 # If nCount must be used in its "proper"
    function, we have to pad out the number of trace calls to one beyond
    the specified, in order to receive object and method info (see
    CallStack::call(), above).
    aCallers.slice!(nCount .. -1)
    end# end

    aCallStack = Binding::stack_class::new( binding() ) # Pass current
    Binding, giving CallStack access to locals.
    callcc { |cc|
    # Hope springs eternal(!):
    #old_tracefunc = get_trace_func()
    set_trace_func(
    proc {
    # event, file, line, id, bind, classname
    |event, *remaining_args|

    # Capturing frames:
    if( event == 'return' )

    # If frame capture yet enabled:
    if( nSkipFrames == 0 )
    # capture current:
    aCallStack.call( event, *remaining_args )
    else # skip to next
    nSkipFrames -= 1
    end# if

    aCallers.shift()

    # If at toplevel, end ride:
    if( aCallers.length == 0 )
    #set_trace_func( get_trace_func() )
    set_trace_func( nil )

    # -----> Stack unrolling finishes here:
    cc.call()
    end# if
    elsif( event == 'line' )
    # *** This breaks free of inadvertent ensure() or rescue()
    blocks:
    throw( Binding::CallStack::tracing_exception ) #
    ***************
    end# if
    }# proc
    )# set_trace_func

    # Stack unrolling begins here: ----->
    throw( Binding::CallStack::tracing_exception )
    }# callcc

    assert(nSkipFrames)

    # Resume execution in client func:
    aCallStack.ret()
    return( aCallStack )
    end# call_stack()

    # Class instance var:
    @stack_class = Binding::CallStack
    class << self
    attr_accessor :stack_class
    end
    end# Binding

    # </call_stack.rb>

    If the unrolling goes uninterrupted, set_trace_func() gets only
    'return' events. If it reports a 'line' event, we bully our way back
    out with another exception. This also takes care of the related
    potential injudicious use of rescue(). It does not, however, address
    the fact that use of call_stack() will cause else() blocks to *never*
    be executed, and this one may really have me stumped. It was my
    understanding, my impression, at least, that recovering from an
    exception is a traditional and accepted application for a continuation,
    and the use of an else() block presupposes that every exception, or
    whatever you want to call it, is an error--which is inconsistent with
    the language's inclusion of throw() and catch(). In all, this looks
    like a good reason for avoiding else() blocks--and I'm not just being
    sore (I hope).

    -J
     
    Dumaiu, Mar 20, 2006
    #4
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Sascha Ebach

    Need help with with Binding.of_caller

    Sascha Ebach, Aug 5, 2005, in forum: Ruby
    Replies:
    2
    Views:
    96
    Sascha Ebach
    Aug 5, 2005
  2. Mauricio Fernandez
    Replies:
    0
    Views:
    114
    Mauricio Fernandez
    Sep 8, 2006
  3. Benjamin Paul Kay

    help with call_stack

    Benjamin Paul Kay, Apr 15, 2007, in forum: Ruby
    Replies:
    0
    Views:
    93
    Benjamin Paul Kay
    Apr 15, 2007
  4. Trans

    Current Binding.of_caller

    Trans, Jun 29, 2007, in forum: Ruby
    Replies:
    2
    Views:
    98
    gabriele renzi
    Jun 29, 2007
  5. Doug
    Replies:
    15
    Views:
    212
    Charles Oliver Nutter
    Jan 13, 2009
Loading...

Share This Page