Project

General

Profile

Bug #18487

Updated by alanwu (Alan Wu) over 2 years ago

Recently I [discovered] that one could use `Kernel#binding` to capture the 
 environment of a frame that is not directly below the stack frame for 
 `Kernel#binding`. I've known that C extensions have this [privilege] for a 
 while, but didn't realize that it was also possible using only the core 
 library. This is a powerful primitive that allows for some funky programs: 

 ```ruby 
 def lookup(hash, key) 
   hash[key] 
   hash 
 end 

 p lookup( 
   Hash.new(&( 
     Kernel.instance_method(:send).method(:bind_call).to_proc >> 
       ->(binding) { binding.local_variable_set(:hash, :action_at_a_distance!) } 
     ) 
   ), 
   :binding 
 ) # => :action_at_a_distance! 
 ``` 

 There might be ways to compose core library procs such that it's less contrived 
 and more useful, but I I'm couldn't figure out a way to do it. Maybe there is a 
 way to make a "local variable set" proc that takes only a name-value pair and 
 no block? 

 ### What's the big deal? 

 This behavior makes the implementation language of methods part of the API 
 surface for `Kernel#binding`. In other words, merely changing a Ruby method to 
 be a C method can be a breaking change for the purposes of `Kernel#binding`, 
 even if the method behaves the same in all other observable ways. I feel that 
 whether a method is native or not should be an implementation detail and should 
 not impact `Kernel#binding`. 

 This is a problem for Ruby implementations that want to implement many core 
 methods in Ruby, because they risk breaking compatibility with CRuby. 
 TruffleRuby has this [problem][privilege] as I alluded to earlier, and CRuby 
 risks making unintentional breaking changes as more methods change to become 
 Ruby methods in the core library. 

 ### Leaking less details 

 I think a straight forward way to fix this issue is by making it so that 
 `Kernel#binding` only ever looks at the stack frame directly below it. If the 
 frame below is a not a Ruby frame, it can return an empty binding. I haven't 
 done the leg work of figuring out how hard this would be to implement in CRuby, 
 though. This new behavior allows observing the identity of native frames, which 
 is new.  

 ### Does the more restrictive behavior help YJIT? 

 Maybe. It's hard to tell without building out more optimizations that are 
 related to local variables. YJIT currently doesn't do much in that area. If I 
 had to guess I wouuld say the more restrictive semantics at least open up the 
 possibility of some deoptimization strategies that are more memory efficient.  

 ### What do you think? 

 This is not a huge issue, but it might be nice to start thinking about for the 
 next release. If a lot of people actually rely on the current behavior we can 
 provide a migration plan. Since it might take years to land, I would like to 
 solicit feedback now. 


 [discovered]: https://github.com/ruby/ruby/commit/54c91042ed61a869d4a66fc089b21f56d165265f 
 [privilege]: https://github.com/oracle/truffleruby/issues/2171 

Back