def myEval(str)
puts "Eval: #{str}"
eval(str)
end
def someOtherScope
localVar = 42
ans = myEval("localVar * 2")
end
The problem is that myEval has no access to it's caller binding. This is some
magic that Kernel::eval is able to perform but ruby is not.
So I came up with some solutions, with a variety of tradeoffs.
class Magic
def myMethod()
bndg = bindingOfCaller
puts "Local vars: #{bndg.local_variables}"
end
end
You can do:
# Idea stolen from: https://bugs.ruby-lang.org/issues/18487
class Magic
define_singleton_method :myMethod, Object::method(:binding) >> ->(bndg) {
puts "Local vars: #{bndg.local_variables}"
...
}
end
The problem is that, because we are sneaking in the call to Kernel::binding by taking
it's method and making it a composition with our proc, but if we pass args in then they
have to go through Kernel::binding as well. I've tried some experiments with either
coming up with a new type of composition for Proc or even updating Kernel::binding to
be able to take and pass on args (as well as the binding) but have not succeeded yet.
If you can figure this out, please let me know!
Here's some notes on what is happening here:
First uses Object::method to look up the Kernel::binding method (not actually call it).
The Proc#>> method is composition: Given f and g are procs (that can be called)
"f >> g" creates a new proc that takes args that does "g(f(args))"
So we basically turn the Kernel::binding method into a proc which we compose
with our lambda so we call lambda(Kernel::binding), but somehow we have
to add our arguments to that.
#########################
# Binding stack support
#########################
# Hash of template fiber (nil for main) to array of bindings
# Fiber aware (keeps a separate stack for each Fiber)
@@bindingStackTracer = nil
# Turn on callerBinding tracing for a block of code.
# This allows us to get the binding() for whomever
# called us, which is needed by the 'compile()' functions.
# Unfortunately, this is massively slow - so it's been replaced
# with the mostly functional and faster callerBinding extension
#
# This allows us to enable tracing the bindings for every callee.
# If given a block, then will execute the block and then disable after.
#
# This code is shockingly slow - we were seeing a 5x slowdown in
# general runs just by enabling this across our fiber code.
def enableBindingTracing(&block)
unless @@bindingStackTracer
@@bindingStack = {}
@@bindingStackTracer = TracePoint.new(:call, :return, :b_call, :b_return) { |tp|
tmpl = Fiber::current
if tmpl.is_a? Template
event = tp.event
@@bindingStack[tmpl] ||= []
@@bindingStack[tmpl].push(tp.binding) if event==:call || event==:b_call
@@bindingStack[tmpl].pop if event==:return || event==:b_return
end
}
# We need to clear @@bindingStack (at least the temlates) on reset
PhotonTest::onReset { @@bindingStack = Hash.new }
end
Verif::assert(!@@bindingStackTracer.enabled?) { "Tried to disable already disabled bindingTracer?" }
@@bindingStackTracer.enable
return unless block
yield
@@bindingStackTracer.disable
end
def disableBindingTracing
Verif::assert(@@bindingStackTracer.enabled?) { "Tried to disable already disabled bindingTracer?" }
@@bindingStackTracer.disable
end
# Find out the binding of a caller, only works in sections
# we have used enableBindingTracing. This is massively
# slow and is generally replaced by the callerBinding extension
def bindingOfCaller(back = -1)
Verif::assert(@@bindingStackTracer) { "Trying to get caller bindings when caller binding tracing is not enabled" }
# Now look through the @@bindingStack for this template
tmpl = Template::curr
return nil unless @@bindingStack[tmpl]
# -1 is the last push which is now
# -2 is the caller of *this*
# Usually they want *their* caller, which is -3
@@bindingStack[tmpl][-2 + back]
end
def myEval(args..., callerBinding)
..
end
# This tells our C extension to put a wrapper around myEval that feeds it the binding of the caller
Caller::provideBinding(self, :myEval)
Feel free to download the C code
You can make it as an extension with a simple extconf.rb of:
require 'mkmf'
create_makefile('callerBinding')
Then you do: "ruby extconf.rb ; make" and then you need to "require 'callerBinding'" in your ruby code.
def bindingOfCaller
# frame 0 is the C ext, frame 1 is this code, frame 2 is our caller, frame 3 is the one we want
RubyVM::CallerBinding.open { |dc| return dc.frame_binding(3) }
end
And then use the following extension.
(Sorry about the mixing of snake_case and camelCase, I prefer to just
use camelCase for all of my code even though the ruby standard is to
switch back and forth - and this is an edit of someone else's code...)
Back to Solutions.