From: Dumaiu Date: 2006-03-21T03:34:11+09:00 Subject: Re: Review requested: Binding.call_stack(), Binding.of_caller()-style 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. # # 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 :object 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 # 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