def myEval(str) puts "Eval: #{str}" eval(str) end def someOtherScope localVar = 42 ans = myEval("localVar * 2") endThe 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 endYou 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}" ... } endThe 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) } endAnd 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.