7

In the following Ruby code,

#! /usr/bin/env ruby
x = true
y = x and z = y
puts "z: #{z}"

It will output z: true, as expected.

But in the following one, which I expect to have the same behavior:

#! /usr/bin/env ruby
x = true
z = y if y = x
puts "z: #{z}"

It results in

undefined local variable or method 'y' for main:Object (NameError)

Why is that?

I understood I am doing an assignment, and implicitly check for the assignment value to determine whether to run z = y. I also understood that if I add declaration of y, y = nil, right after the x = 5 line, it will pass and run as expected.

But isn't it correct to expect that the language should evaluate the if part first and then its content, and second chunk of code to behave the same as the first chunk of code?

Ry-
  • 218,210
  • 55
  • 464
  • 476
Jimmy Chu
  • 972
  • 8
  • 27
  • 1
    That’s a really good question, given that `z = 2 if y = x` works fine, and `z = w if false` does too… – Ry- Sep 13 '14 at 02:05
  • 4
    This is one those stupid areas with Ruby. There is some ill-defined clause which amounts to "if the parser encounters the assignment" (meaning the assignment does *not* have to be executed to create a local binding) .. except it breaks down when in cases like this, and several others. Ref. http://stackoverflow.com/questions/25783428/why-is-a-variable-declared-when-it-appears-in-if-false – user2864740 Sep 13 '14 at 02:07
  • I'm not a Ruby expert, but this seems like a bug in the parser. – Marc Baumbach Sep 13 '14 at 02:08
  • 1
    @MarcBaumbach It's not a "bug". It's "expected behavior", even if not really .. expected. – user2864740 Sep 13 '14 at 02:10
  • @user2864740, agreed. It's effectively equivalent to `if y = x; z = y; end` to the programmer but produces a different result. While the example may be contrived, it's a definite gotcha. – Marc Baumbach Sep 13 '14 at 02:13
  • Thank you all. I am learning something from you guys. – Jimmy Chu Sep 13 '14 at 02:15
  • 2
    I assume the same gremlins that are responsible for bare `x=x` not being a `NameError` have their claws in this one. – roippi Sep 13 '14 at 02:19
  • 2
    @roippi `x=x` is not an error because the assignment `x =` precedes the call `x`. – sawa Sep 13 '14 at 02:28
  • 1
    @user2864740: Come on, we're allowed to like Ruby and call its stupid things bugs at the same time. – mu is too short Sep 13 '14 at 03:27
  • @muistooshort Maybe a "design shortcoming" or "quirky non-formalized behavior"? :D – user2864740 Sep 13 '14 at 04:14
  • 1
    Rubinius does the right thing. MRI and JRuby raise NameError. I didn't test any other implementations. YMMV. – Todd A. Jacobs Sep 13 '14 at 06:03
  • 1
    @sawa Come on. No one expects `x = x` to assign nil to *x*; it *should* raise NameError with a sane parser. – Todd A. Jacobs Sep 13 '14 at 06:28

1 Answers1

4

TL;DR

This is actually interpreter-specific. The problem shows up in MRI Ruby 2.1.2 and JRuby 1.7.13, but works as expected in Rubinius. For example, with Rubinius 2.2.10:

x = true
z = y if y = x
#=> true

In MRI, a little exploration with Ripper shows that Ruby treats the post-condition differently even though the AST assignments are similar. It actually uses different tokens for post-conditions when building the AST, and this appears to have an effect on the evaluation order of assignment expressions. Whether or not this should be the case, or whether it can be fixed, is a question for the Ruby Core Team.

Why It Works with a Logical And

x = true
y = x and z = y

This succeeds because it's really two assignments in sequence, because true is assigned to x and therefore evaluates as truthy. Since the first expression is truthy, the next expression connected by the logical and is also evaluated and likewise evaluates as truthy.

y = x
#=> true

z = y
#=> true

In other words, x is assigned the value true, and then z is also assigned the value true. At no point is the right-hand side of either assignment undefined.

Why It Fails with a Post-Condition

x = true
z = y if y = x

In this case, the post-condition is actually evaluated first. You can see this by looking at the AST:

require 'pp'
require 'ripper'

x = true

pp Ripper.sexp 'z = y if y = x'
[:program,
 [[:if_mod,
   [:assign,
    [:var_field, [:@ident, "y", [1, 9]]],
    [:vcall, [:@ident, "x", [1, 13]]]],
   [:assign,
    [:var_field, [:@ident, "z", [1, 0]]],
    [:vcall, [:@ident, "y", [1, 4]]]]]]]

Unlike your first example, where y was assigned true in the first expression, and therefore resolved to true in the second expression before being assigned to z, in this case y is evaluated while still undefined. This raises a NameError.

Of course, one could legitimately argue that both expressions contain assignments, and that y wouldn't really be undefined if Ruby's parser evaluated y = x first as it does with a normal if statement (see AST below). This is probably just a quirk of post-condition if statements and the way Ruby handles the :if_mod token.

Succeed With :if Instead of :if_mod Tokens

If you reverse the logic and use a normal if statement, it works fine:

x = true
if y = x
  z = y
end
#=> true

Looking at Ripper yields the following AST:

require 'pp'
require 'ripper'

x = true

pp Ripper.sexp 'if y = x; z = y; end'
[:program,
 [[:if,
   [:assign,
    [:var_field, [:@ident, "y", [1, 3]]],
    [:vcall, [:@ident, "x", [1, 7]]]],
   [[:assign,
     [:var_field, [:@ident, "z", [1, 10]]],
     [:var_ref, [:@ident, "y", [1, 14]]]]],
   nil]]]

Note that the only real difference is that the example that raises NameError uses :if_mod, while the version that succeeds uses :if. It certainly seems like the post-condition is the cause of the bug, quirk, or misfeature that you're seeing.

What to Do About It

There may be a good technical reason for this parsing behavior, or there may not. I'm not qualified to judge. However, if it looks like a bug to you, and you're motivated to do something about it, the best thing to do would be to check the Ruby Issue Tracker to see if it's already been reported. If not, maybe it's time someone brought it up formally.

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199