Thursday, November 10, 2016

x == x = WTF?!

   A short time ago in a project close by, someone (not me, but ironically enough someone who has been hacking Ruby about twice as long as me) had committed code similar to:
if status == status = 1 || status = 2 || status = 3
   That probably makes most of you go WTF, and you probably figure the author meant "if status == 1 || status == 2 || status == 3" but somehow screwed up to the point of insanity, and rightly so.   But bear with me, it's a fun little exploration of some bizarre Ruby usage.

   First, since "status = 1" would always return a truthy value, the rest of it would get short-circuited.  So, we can omit the rest from further investigation, and just use "status == status = 1".

  But more importantly, this got me thinking, what would that even DO?  The rest of this post is a quote from the analysis I wrote up just because I thought he would find it amusing.  If anybody is familiar with how MRI works under the hood, please let me know if I'm guessing right!

===8<---cut here---

This LOOKS like it might mean "assign status, and check if it still has the value we just assigned to it", but after some experimentation in irb, it's really "assign status, and check if that matches its previous value".  Enlightening experimental irb session:

$ irb
:001 > x == x = 1
NameError: undefined local variable or method `x' for main:Object
Did you mean?  x
        from (irb):1 [...]
:002 > x
 => nil
:003 > x == x = 1
 => false
:004 > x
 => 1
:005 > x == x = 1
 => true
:006 >
At line 001, we get a NameError because we haven't told it about x.   At 002, x now exists (but is nil), because referencing it brought it to Ruby's attention.  At 003, there is no NameError, so it can get to the assignment, but the old value was nil, so the comparison is effectively "nil == 1".  At 004, we can see that it did indeed get assigned 1.  At 005, the values finally match.

Apparently the == receives the original value of status, as though the order of ops under the hood is "push status, assign status, push status, call ==".  With most Ruby objects, this would always result in true... but Fixnums are odd ducks, in that they are all unique values.  1 is 1 (and all alone) and ever more shall be so.  ;-)  Any Ruby object that is a Fixnum with the value of 1 is the same object and will never have another value.  If you reassign the variable, you're setting the variable itself to refer to a different object.  In C terms, you're changing the pointer, not some field in what it points to.   So, the odd-numbered lines above were equivalent to "push something that doesn't exist so barf with a NameError", "push nil, assign x 1, push 1, call ==", and "push 1, assign x 1, push 1, call ==".   If we were to now do "x == x = 2", it would be "push 1, assign x 2, push 2, call ==", so that the comparison would fail but afterwards we would find that x is indeed 2 -- and doing that same Ruby command again would succeed the second time, as it would then start with "push 2".  Mind you though, that's in MRI; I have no idea if it's the same in mruby.

No comments:

Post a Comment