Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Undefined local variable based on syntax in Ruby

Tags:

ruby

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?

like image 298
Jimmy Chu Avatar asked Sep 13 '14 01:09

Jimmy Chu


1 Answers

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.

like image 68
Todd A. Jacobs Avatar answered Oct 16 '22 13:10

Todd A. Jacobs