Why can I refer to a variable outside of an if/unless/case statement that never ran?

Solution 1:

It's because of how the Ruby parser works. Variables are defined by the parser, which walks through the code line-by-line, regardless of whether it will actually be executed.

Once the parser sees x =, it defines the local variable x (with value nil) henceforth in the current scope. Since if/unless/case/for/while do not create a new scope, x is defined and available outside the code block. And since the inner block is never evaluated as the conditional is false, x is not assigned to (and is thus nil).

Here's a similar example:

defined?(x) and x = 0
x  #=> nil

Note that this is a rather high-level overview of what happens, and isn't necessarily exactly how the parser works.

Solution 2:

This has to do with a quirk of Ruby's scoping rules.

In ruby, an undecorated variable x appearing by itself could either be a local variable or a method call -- the grammar can't tell which. It's up to the parser to figure it out as it resolves local variable references. The rule is simple: if an assignment to a variable of the same name has been seen already in the local scope, then the reference is a local variable, and the reference is bound to that local variable. Otherwise, it's a method call, and it will be looked up as such at runtime.

Local variable references in Ruby are optimized into array lookups (each local variable is assigned a 'slot', and bound local variable references generated by the parser are converted into slot references). The array is initialized with all nil:

/* initialize local variables */
for (i=0; i < local_size; i++) {
    *sp++ = Qnil;
}

Thus, if you refer to a local variable that hasn't been assigned, through a bound local reference (which can only happen if there was a skipped assignment above the reference in the same local scope), you get nil.

Solution 3:

I thought your question was interesting so I tried to look it up and found this: I don't understand ruby local scope

The correct answer seems to be put Jorg.

Lets look at what happens when you try to access a variable that isn't initialized:

NameError: undefined local variable or method `UNDECLAREDVAR' for main:Object

The exception states that it is unavailable to evaluate whether a variable or method. The reason it doesn't throw the same exception is because uninitialized local variables are set to nil. So puts x is a okay because the interpretor knows that x is variable but uninitialized and not a method.