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?
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With