Unfortunately there are no suitable keywords in ruby which might explain it beyond the doubt. So, let me translate it to javascript! (Javascript local variables are practically identical to ruby variables - with an exception for explicit creation keywords)
But before, few notes about scopes in javascript. Even though I'll be writing using JS, all the notes are also correct for ruby - except for a lack of the explicit variable keyword.
So: let keyword - let creates a new variable on the current scope. Once created, given variable can be read only when we are inside the same lexical scope or its child scopes:
let x = 0;
x #=> 0;
function() { # function creates a new child scope
return x; # access variable of the parent scope
}() #=> 0
It is important to understand that the scope is lexical, not dynamic - this means that variables resolves in a static context, which depends on the structure of the code rather than how the code is being called. This creates so-called closures (again - closures also exists in Ruby, however, unlike in JS, it's best to avoid them).
When searching for a variable, we always look at the current scope first and, if we have nothing defined in current scope, move to the parent scope. If no parent scope is found, exception is thrown.
So, let's translate your code into javascript:
let x = 0, y = 0, z = 0
let ary = [1, 2, 3]
ary.forEach(function(x) { # x is now function argument
let y; # This is because of `|...; y|` in your block
# In this scope we have 3 variables:
# x - is an argument of a function
# y - is locally scoped variable
# z - is undefined in this scope, so it'll reference z from the parent scope
y = x
z = x
console.log(x, y, z)
})
console.log(x, y, z)
Let's analyze. In the above code, there are two scopes - top scope and child scope. There are 4 variables defined in your top scope (x, y, z and ary) and 2 variables defined in the child scope (x, y).
When you do y = x you first read the value of x - since such a variable exists in the current scope (and it is passed as an argument), we take that. In first iteration the value of x is 1, so expression evaluates to:
y = 1
Now we need to find variable y we want to assign to - there is variable y in the current scope so we assign to that one. The y variable in the main scope is not affected by this assignment
Next expression: z = x - x resolves the same way as before, so in first iteration we have:
z = 1
But now, there is no z variable in the current scope, so we look for a variable in the parent scope. In result, this assignment modifies the variable in the main scope
Next, we're printing x, y and z resolved in the current scope - there's no surprise here, they are all 1s.
Second iteration, everything repeats, but this time x resolves to 2. Again, we set the value of a y in a local, child scope and z of the parent scope. And then, third iteration.
In last expression we print the values of the variables, but in the parent scope. x is 0 as we have never done a single assignment to it, similarly y is 0 - because all the assignments were done against the variable defined in the child scope so, in fact, we didn't make a single assignment to it. z on the other hand was assigned to on each iteration. On the last iteration it received value 3 which is what it currently holds.