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?

12
  • 1
    That’s a really good question, given that z = 2 if y = x works fine, and z = w if false does too… Commented Sep 13, 2014 at 2: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. stackoverflow.com/questions/25783428/… Commented Sep 13, 2014 at 2:07
  • 1
    @MarcBaumbach It's not a "bug". It's "expected behavior", even if not really .. expected. Commented Sep 13, 2014 at 2:10
  • 2
    I assume the same gremlins that are responsible for bare x=x not being a NameError have their claws in this one. Commented Sep 13, 2014 at 2:19
  • 2
    @roippi x=x is not an error because the assignment x = precedes the call x. Commented Sep 13, 2014 at 2:28

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Rubinius uses :if instead of :if_mod in its post-condition AST: require 'pp'; require 'parser/current'; pp Parser::CurrentRuby.parse 'z = y if y = x'.
Good details .. and good job staying out of the fray. I think I now finally have a link-to question for when this comes up again.
This looks like a bug in Rubinius.
@JörgWMittag Why ? As condition of inline if is processed first, it does make sense parser should handle it similar.
Related discussion on Twitter that explains this as an LALR descent issue with the :if_mod token.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.