A Question Asker
A Question Asker

Reputation: 3323

Is there a ruby hook for the variable that was just allocated?

Here is what I'd ideally like. The user does:

a="hello"

and the output would be

You just allocated "a" !
 => "Hello" 

Order is irrelevant as long as I can make that message happen.

Upvotes: 4

Views: 626

Answers (2)

user47322
user47322

Reputation:

No, there's no straight-forward way to make this happen as local variable names are discarded by the Ruby bytecode compiler before your code is executed.

The only instructions YARV (the Ruby VM in use in MRI 1.9.2) provides regarding local variables are getlocal and setlocal, which both operate on integer indicies rather than variable names. Here's an excerpt from insns.def in the 1.9.2 source:

/**********************************************************/
/* deal with variables                                    */
/**********************************************************/

/**
  @c variable
  @e get local variable value (which is pointed by idx).
  @j idx 
 */
DEFINE_INSN
getlocal
(lindex_t idx)
()
(VALUE val)
{
    val = *(GET_LFP() - idx);
}

/**
  @c variable
  @e set local variable value (which is pointed by idx) as val.
  @j idx 
 */
DEFINE_INSN
setlocal
(lindex_t idx)
(VALUE val)
()
{
    (*(GET_LFP() - idx)) = val;
}

It might be possible to hack the MRI source (or use set_trace_func and dive into a Binding object - see sarnold's answer) to inform you when a local variable is set, but there isn't any high-level way of doing it and you probably won't be able to retrieve the names of those local variables without diving into the interpreter internals.

Upvotes: 7

sarnold
sarnold

Reputation: 104080

I've come up with a solution that is based on set_trace_func. I can't actually counter the limitations Charlie points out but I believe what I have written should work more or less as you described:

#!/usr/bin/ruby

def hash_from_binding(bin)
    h = Hash.new
    bin.eval("local_variables").each do |i|
        v = bin.eval(i)
        v && h[i]=bin.eval(i)
    end
    bin.eval("instance_variables").each do |i|
        v = bin.eval(i)
        v && h[i]=bin.eval(i)
    end
    h
end


$old_binding = hash_from_binding(binding)
$new_binding = hash_from_binding(binding)

set_trace_func lambda {|event, file, line, id, bin, classname|
  $old_binding = $new_binding
  $new_binding = hash_from_binding(bin)

  diff = $new_binding.reject {|k, v| $old_binding[k] == $new_binding[k]}

  printf("%d:\n", line)
#  $old_binding.each do |k,v|
#     printf("%8s: %s\n", k, v)
#  end
#  $new_binding.each do |k,v|
#     printf("%8s: %s\n", k, v)
#  end
  diff.each do |k,v|
      printf("%8s: %s\n", k, v)
  end
}

a = "hello"
b = "world"
c = "there"
d = nil
e = false
@a = "HELLO"
@b = "WORLD"
A="Hello"
B="World"

def foo
    foo_a = "foo"
    @foo_b = "foo"
end

foo

hash_from_binding(bin) will turn a Binding object into a Hash. You can remove the instance_variables portion if you don't want those. You can remove the local_variables portion if you don't want those. The complication of v && h[i]=bin.eval(i) is due to an oddity in the Binding objects -- even though the tracing function hasn't yet "parsed" through all the content, the Binding object passed to the tracing function does know about all the variables that are going to be defined in the scope. It's awkward. This at least filters out the variables that haven't been assigned a value. By consequence it also filters out variables assigned values nil or false. You might be content with the reject action in the tracing function to do the filtering work for you.

The set_trace_func API will call a tracing method for every source line that is parsed. (This might be a drastic limitation in the face of different execution environments.) So I wrote a tracing function that will compare the old bindings object against a new bindings object and report the variable definitions that are changed. You could also report the variable definitions that are new, but that would miss cases like:

a = 1
a = 2

One funny consequence is that the bindings reported change drastically across function calls as new variables are brought to life and old variables are removed from the environment. This may overly confuse the output, but perhaps the event parameter may be of use in determining whether to print new variable values. (Since the function call might modify variable values in the scope of the "returnee" code, printing them all seems like the safe approach.)

When the tool is run upon itself, it outputs the following:

$ ./binding.rb 
38:
39:
       a: hello
40:
       b: world
41:
       c: there
42:
43:
44:
      @a: HELLO
45:
      @b: WORLD
46:
48:
48:
48:
53:
      @a: HELLO
      @b: WORLD
48:
49:
50:
   foo_a: foo
50:
  @foo_b: foo
$ 

This is the most complicated piece of Ruby code I've run this tool upon, so it might break on something non-trivial.

Upvotes: 3

Related Questions