Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding a hash and making [] operators private - cant use ||= anymore

Test code:

class PrivHash < Hash
  def set(key, val)
     self[key] = val
  end
  def set_maybe(key, val)
    self[key] ||= val
  end
  private
  def []= key, value 
  end
  def [] key
    super            
  end
end

With this code, I expect both set and set_maybe to work. However, only set works and set_maybe fails with:

[30] pry(#<TranslationInfo>):1> ph.set_maybe(:a, 1)
NoMethodError: private method `[]' called for {:a=>2}:#Class:0x007f99c5924c38>::PrivHash 
from (pry):56:in `set_maybe'

I assumed that self[:b] ||= <x> is just syntactic sugar for self[:b] || self[:b] = <x>, but I guess it isn't because this works.

What bugs me is why I am getting this error.. I am executing this from within the class so why am I getting private method error?

like image 688
Karthik T Avatar asked May 10 '16 05:05

Karthik T


2 Answers

Handling of private methods is a bit of a mess, currently.

The original rule was:

private methods can only be called without an explicit receiver.

This is a nice, simple, easily understandable rule. It is also a static rule, i.e. it can be checked without running the code, in fact, it is even a syntactic rule, it doesn't even need sophisticated static analysis, it can be checked in the parser.

However, it was soon noticed that this rule makes it impossible to call private setters, since setters can't be called without an explicit receiver (foo = bar is a setting a local variable, not calling a setter). Ergo, the rule was extended:

private methods can only be called without an explicit receiver, unless the method call is an assignment method call, in which case the method can also be called with an explicit receiver as long as that explicit receiver is the literal pseudo-variable self.

This allows you to call private setters with an explicit receiver of the literal value self:

self.foo = bar

but not a dynamic value of self

baz = self
baz.foo = bar # NoMethodError: private method `foo=' called

This still preserves the property that private method calls can be detected at parse time.

Two years ago, I filed a bug about abbreviated method assignments not working, i.e.:

self.foo += bar # NoMethodError

That bug was fixed by again extending the rule for private method calls (and now the rule is already getting so complex that I'm not going to spell it out).

However, there are still a lot of cases left that are not covered by the existing rules, where methods simply cannot syntactically be called without an explicit receiver and thus cannot be private:

self[foo]
!self
self + foo

etc.

Some of those have been fixed, some haven't. The problem is that the rule has now gotten so complex that it is hard to implement correctly. There have been proposals to change the rule to something like this:

private methods can only be called without an explicit receiver or an explicit receiver which is the literal pseudo-variable self.

That is a nice, simple, easily understandable rule, can be statically checked at parse time, and has none of the complex exceptions and corner cases we have currently. It is, however, not yet implemented AFAIK.

like image 54
Jörg W Mittag Avatar answered Nov 15 '22 05:11

Jörg W Mittag


I tried to decompile it.

code = <<CODE
class PrivHash < Hash
  def set(key, val)
    self[key] = val
  end
  def set_maybe(key, val)
    self[key] ||= val
  end
  private
  def []= key, value 
  end
  def [] key
    super            
  end
end
CODE
disasm = RubyVM::InstructionSequence.compile(code).disasm
File.write("#{RUBY_VERSION}.txt", disasm)

Based on the results, I conclude the issue is this: 2.2.0 calls

0010 opt_aref         <callinfo!mid:[], argc:1, FCALL|ARGS_SIMPLE>
...
0013 branchif         25
...
0020 opt_aset         <callinfo!mid:[]=, argc:2, FCALL|ARGS_SIMPLE>

Basically, evaluate [], see if it's falsy, and if so, call []=. But 2.3.0 doesn't use FCALL flag on the [] call:

0010 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
...
0014 branchif         27
...
0021 opt_aset         <callinfo!mid:[]=, argc:2, FCALL|ARGS_SIMPLE>, <callcache>

The FCALL flag identifies a call with an implicit receiver (foo()); without FCALL, the call is to an explicit receiver (self.foo() or bar.foo()), which is prohibited by private methods.

Now, why 2.3.0 does this... No idea.

like image 45
Amadan Avatar answered Nov 15 '22 07:11

Amadan