How does Ruby handle variable assignment in dead code? [duplicate] - ruby

This question already has answers here:
Why can I refer to a variable outside of an if/unless/case statement that never ran?
(3 answers)
Closed 5 years ago.
We define a function foo:
def foo(s)
case s
when'foo'
x = 3
puts x.inspect
when 'bar'
y = 4
puts y.inspect
end
puts x.inspect
puts y.inspect
end
We then call it as follows:
1.9.3p194 :017 > foo('foo')
in foo scope
3
in outer scope
3
nil
=> nil
1.9.3p194 :018 > foo('bar')
in bar scope
3
in outer scope
nil
3
=> nil
Why does the function not throw an error about an unregistered local variable in either case? In the first case, the variable y seems like it should not exist, so you can't call inspect on it in the outer scope; the same for x in the second case.
Here's another similar example:
def test1
x = 5 if false
puts x.inspect
end
def test2
puts x.inspect
end
And then:
1.9.3p194 :028 > test1
nil
=> nil
1.9.3p194 :029 > test2
NameError: undefined local variable or method `x' for main:Object
What's going on here? It seems like Ruby is hoisting the variable declaration into the outer scope, but I wasn't aware that this is something Ruby does. (Searching for "ruby hoisting" only turns up results about JavaScript hoisting.)

When the Ruby parser sees the sequence identifier, equal-sign, value,
as in this expression
x = 1
it allocates space for a local variable called x. The creation of the
variable—not the assignment of a value to it, but the internal
creation of a variable—always takes place as a result of this kind of
expression, even if the code isn’t executed! Consider this example:
if false
x = 1
end
p x # Output: nil
p y # Fatal Error: y is unknown
The assignment to x isn’t executed, because it’s wrapped in a failing
conditional test. But the Ruby parser sees the sequence x = 1, from
which it deduces that the program involves a local variable x. The
parser doesn’t care whether x is ever assigned a value. Its job is
just to scour the code for local variables for which space needs to
be allocated. The result is that x inhabits a strange kind of variable limbo.
It has been brought into being and initialized to nil.
In that respect, it differs from a variable that has no existence at
all; as you can see in the example, examining x gives you the value
nil, whereas trying to inspect the non-existent variable y results
in a fatal error. But although x exists, it has not played any role in
the program. It exists only as an artifact of the parsing process.
Well-Grounded Rubyist chapter 6.1.2

The ruby parser goes through every lines and set to nil all variable =. The code being actually executed or not does not matter.
See Why can I refer to a variable outside of an if/unless/case statement that never ran?

Related

What does shorthand operator like (+=) in ruby really does?

While I can understand that x += 1 is equivalent to x = x + 1, I'm interested what it does in the background.
I've tried in irb with x += 1 without first assigning value to x and surely I get an error, it says
NoMethodError (undefined method `+' for nil:NilClass)
When I tried checking x value after it errored, it's now a nil value which isn't quite clear when it was assigned, unless it's done by x += 1 statement.
So, I'm trying to understand how the += does thing as I could not find the documentation for it or it being buried behind the lot of ruby classes. Any pointer would be appreciated.
Ruby treats identifiers as a variables if it sees assignment (=) even if the assignment fails.
irb(main):001:0> a
NameError: undefined local variable or method `a' for main:Object
from (irb):1
from /usr/bin/irb:12:in `<main>'
irb(main):002:0> a = 1 if false
=> nil
irb(main):003:0> a
=> nil
irb(main):004:0>
Even though a=1 never executes, a gets defaulted to nil.
Analysis
Defining our Terms
First of all, + and += are methods, not keywords or operators. More accurately, + is defined as Integer#+, while += is syntactic sugar provided by the interpreter as shorthand for a common operation such as incremented assignment.
Secondly, += and related operations are called abbreviated assignments in Ruby. The full list of supported assignment shorthand items currently include:
["+=",
"-=",
"*=",
"/=",
"%=",
"**=",
"&=",
"|=",
"^=",
"<<=",
">>=",
"||=",
"&&="]
The only non-method operators in Ruby are:
["=", "..", "...", "!", "not", "&&", "and", "||", "or", "!=", "!~"]
and shorthand assignment operators like += are not user-definable.
Defining the Problem
The behavior you're seeing has to do with scope, and how the parser handles block-local variables. Specifically, the Ruby assignment documentation says:
[A] local variable is created when the parser encounters the assignment, not when the assignment occurs[.]
That means that top-level code such as:
x
#=> NameError (undefined local variable or method `x' for main:Object)
raises a NameError exception because defined? x #=> nil. However, the parser auto-instantiates local variables when there's an assignment, so that the following works (sort of), at least in the sense that it doesn't raise NameError:
# x is autovivified as nil
x = x + 1
#=> NoMethodError (undefined method `+' for nil:NilClass)
However, nil doesn't have a NilClass#+ method, so you get NoMethodError instead of NameError. This is because:
nil.respond_to? :+
#=> false
Solutions
The basic solution is to explicitly ensure your variable exists within the current scope, and that it is not nil. You can do this in a variety of ways, but the simplest thing is just to assign it directly. For example:
# assign zero to x unless it's truthy
x ||= 0
# perform incrementing assignment
x += 1
Alternatively, you can drop the shorthand and cast the autovivified x as an Integer, which does have a :+ method. For example, assuming x is currently undefined (e.g. you've restarted irb, or you've set x to nil):
x = x.to_i + 1
#=> 1

Why reference to variable inside not run if body does not raise NoMethodError? [duplicate]

This question already has answers here:
Why can I refer to a variable outside of an if/unless/case statement that never ran?
(3 answers)
Closed 2 years ago.
Hi I got a variable inside an if block that the condition of if is false.
if false
a = 1
end
a => nil
However the a does not raise an NoMethodError instead returns nil?
Why a = 1 Isn't Evaluated
Your code is logically equivalent to both of the following:
a = 1 if false; a
false.eql?(true) && a=(1)
In all three cases, since false is not true, variable a is never assigned. However, the reason it's nil rather than raising NameError is a little non-intuitive.
Why Unassigned Variables Return nil Instead of Raising NameError
In Ruby, non-constant variables are defined when encountered by the parser, rather then when executed by the code path. If the parser encounters a non-constant variable that is not an assignment, it creates the variable and assigns nil to it.
You can still expect to see NameError when the interpreter uses an unassigned variable. For example, in a fresh irb session:
# variable not in scope
puts a
#=> NameError (undefined local variable or method `a' for main:Object)
# variable auto-vivified by parser
a = 1 if false; a
#=> nil
a = 1; a
#=> 1
Related
Why are constants not initialized like local variables when the file is parsed by Ruby?
The Ruby parser keeps track of which names are local variables (vs method calls). Ruby parser doesn’t know if the conditionals will be true/false, so it already got an reference to a.

How is a local variable created even when IF condition evaluates to false in Ruby? [duplicate]

This question already has answers here:
Confusion with the assignment operation inside a falsy `if` block [duplicate]
(3 answers)
Closed 5 years ago.
Try the following in irb: (I'm using Ruby 2.0.0-p247)
blah
#=> NameError: undefined local variable or method `blah' for main:Object
if false
blah = 'blah'
end
#=> nil
blah
#=> nil
I'm surprised that blah is assigned nil even when the if condition evaluates to false.
I thought the code within if is skipped as the condition evaluates to false.
Could someone with Ruby internals knowledge kindly explain how this happened?
Thank you
Local variables in ruby are created during parsing/compilation of code (not execution). They are lexically scoped, so a local variable is not visible before the line where it's assigned to.
defined?(foo) # => nil
if false
defined?(foo) # =>
foo = 'blah'
defined?(foo) # =>
end
defined?(foo) # => "local-variable"
foo # => nil
defined?(foo) lines inside of if return nothing, because they didn't run. The assignment wasn't executed as well. However, the compiler saw the assignment to local variable and created one (with default value of nil).
This behaviour explains the trick from WAT talk:
a = a # => nil
Even though variable a doesn't exist, it is created (and set to nil) right before this line, simply because there is an assignment expression in the code (target of which is yet unknown local variable). So by the time the right hand side of this expression is evaluated, a exists.

Why x = x does not raise an error even if x is undefined [duplicate]

This question already has an answer here:
Closed 10 years ago.
Possible Duplicate:
Why is `a = a` `nil` in Ruby?
I'm sure there is a reason for this behavior i'm just curious what it is.
y = x # NameError: undefined local variable or method 'x'
x = x # => nil
This is caused by the way variables are initialized in Ruby, which is rather unique to this language. Basically, Ruby initializes (creates) a variable if it could possibly get assigned a value. Consider this example:
if false
x = "hello"
end
x will definitely not get assigned the "hello" string here. However, it will still get initialized with nil as from the static program analysis, it could have been assigned.
Your example is similar. Because you assign something to x, it will get initialized with nil before the statement is executed. Thus, during execution, x is in fact nil.

Why does Ruby seem to hoist variable declarations from inside a case statement even if that code path is not executed? [duplicate]

This question already has answers here:
Why can I refer to a variable outside of an if/unless/case statement that never ran?
(3 answers)
Closed 5 years ago.
We define a function foo:
def foo(s)
case s
when'foo'
x = 3
puts x.inspect
when 'bar'
y = 4
puts y.inspect
end
puts x.inspect
puts y.inspect
end
We then call it as follows:
1.9.3p194 :017 > foo('foo')
in foo scope
3
in outer scope
3
nil
=> nil
1.9.3p194 :018 > foo('bar')
in bar scope
3
in outer scope
nil
3
=> nil
Why does the function not throw an error about an unregistered local variable in either case? In the first case, the variable y seems like it should not exist, so you can't call inspect on it in the outer scope; the same for x in the second case.
Here's another similar example:
def test1
x = 5 if false
puts x.inspect
end
def test2
puts x.inspect
end
And then:
1.9.3p194 :028 > test1
nil
=> nil
1.9.3p194 :029 > test2
NameError: undefined local variable or method `x' for main:Object
What's going on here? It seems like Ruby is hoisting the variable declaration into the outer scope, but I wasn't aware that this is something Ruby does. (Searching for "ruby hoisting" only turns up results about JavaScript hoisting.)
When the Ruby parser sees the sequence identifier, equal-sign, value,
as in this expression
x = 1
it allocates space for a local variable called x. The creation of the
variable—not the assignment of a value to it, but the internal
creation of a variable—always takes place as a result of this kind of
expression, even if the code isn’t executed! Consider this example:
if false
x = 1
end
p x # Output: nil
p y # Fatal Error: y is unknown
The assignment to x isn’t executed, because it’s wrapped in a failing
conditional test. But the Ruby parser sees the sequence x = 1, from
which it deduces that the program involves a local variable x. The
parser doesn’t care whether x is ever assigned a value. Its job is
just to scour the code for local variables for which space needs to
be allocated. The result is that x inhabits a strange kind of variable limbo.
It has been brought into being and initialized to nil.
In that respect, it differs from a variable that has no existence at
all; as you can see in the example, examining x gives you the value
nil, whereas trying to inspect the non-existent variable y results
in a fatal error. But although x exists, it has not played any role in
the program. It exists only as an artifact of the parsing process.
Well-Grounded Rubyist chapter 6.1.2
The ruby parser goes through every lines and set to nil all variable =. The code being actually executed or not does not matter.
See Why can I refer to a variable outside of an if/unless/case statement that never ran?

Resources