Reputation: 3323
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
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
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