Project

General

Profile

Actions

Bug #17619

closed

if false foo=42; end creates a foo local variable set to nil

Added by pkmuldoon (Phil Muldoon) 8 months ago. Updated 8 months ago.

Status:
Rejected
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:102438]

Description

Take this following code

[1] pry(main)> defined?(foo)
nil

[2] pry(main)> if false
[2] pry(main)*   foo = 42
[2] pry(main)* end  

[3] pry(main)> defined?(foo)
"local-variable"

The inner scope inherits the parent scope (ok) but also modifies the parent scope even if the child scope is never entered. A lesser effect of this:

[1] pry(main)> defined?(bar)
nil

[2] pry(main)> if false
[2] pry(main)*   bar = 99
[2] pry(main)* end  

[3] pry(main)> defined?(bar)
"local-variable"

[5] pry(main)> bar
99

That somewhat lesser affecting because I can just about accept a variable invading the parent scope, and existing after, as a hoisting event. But surely that should not be the case in the negative program-flow case?

The side effects of this are defined?(foo) can't be trusted anymore.

Apologies if this bug has been filed. I did search for it, but couldn't find anything quite matching it. Thanks!

Updated by pkmuldoon (Phil Muldoon) 8 months ago

This affects Ruby 2.7.1 and also Ruby 3.0.0. I've not had time to test earlier versions.

Updated by zverok (Victor Shepelev) 8 months ago

To the best of my understanding, if doesn't create its own scope with its own local variables. So this if means for the interpreter that "local variable bar is present in current scope" (just not assigned).

Updated by pkmuldoon (Phil Muldoon) 8 months ago

The second code example is incorrect, it should read

1] pry(main)> defined?(bar)
nil

[2] pry(main)> if true
[2] pry(main)*   bar = 99
[2] pry(main)* end  

[3] pry(main)> defined?(bar)
"local-variable"

[5] pry(main)> bar
99

Updated by pkmuldoon (Phil Muldoon) 8 months ago

pkmuldoon (Phil Muldoon) wrote in #note-3:

The second code example is incorrect, it should read

1] pry(main)> defined?(bar)
nil

[2] pry(main)> if true
[2] pry(main)*   bar = 99
[2] pry(main)* end  

[3] pry(main)> defined?(bar)
"local-variable"

[5] pry(main)> bar
99

zverok (Victor Shepelev) wrote in #note-2:

To the best of my understanding, if doesn't create its own scope with its own local variables. So this if means for the interpreter that "local variable bar is present in current scope" (just not assigned).

Okay, but why would a variable be created in a program-flow primitive ie (if etc) if that condition is such that that code is never executed?

Updated by chrisseaton (Chris Seaton) 8 months ago

but why would a variable be created in a program-flow primitive ie (if etc) if that condition is such that that code is never executed?

Local variables are 'created' (we could say 'declared') during parse-time in Ruby. That's why they become defined as soon as they are found lexically. We could also call this 'hoisting'.

Updated by pkmuldoon (Phil Muldoon) 8 months ago

While the hoisting issue is meh to mostly okay, I guess, this block:

if false
   foo = 32
end
defined?(foo)
"local-variable"

is arguably an interpreter side effect that can cause problematic issues to developers. Especially if they use defined?(foo) as a conditional return value. If a condition in a control flow primitive equates to false, arguably, in the case above, that code is being executed, if only partially (the creation of a local variable foo), and causing side effects. If as a developer, I read that code above, I would expect that condition not to be executed, and foo remain undefined. Thanks for looking at this. Your attention to it valued!

Updated by nobu (Nobuyoshi Nakada) 8 months ago

  • Status changed from Open to Rejected

In Ruby, local variables are defined by assignment expressions, and it is never in "undefined" state but is initialized as nil.

Actions #9

Updated by josh.cheek (Josh Cheek) 8 months ago

It's intentional. Eg what if you set the same variable in both branches? Then it would be clearer that you are expecting the variable to be visible outside the scope of the conditional.

Contrived code example:

if eligible?
  bonus = 100
else
  bonus = 0
end
score + bonus

Updated by Student (Nathan Zook) 8 months ago

josh.cheek (Josh Cheek) wrote in #note-9:

It's intentional. Eg what if you set the same variable in both branches? Then it would be clearer that you are expecting the variable to be visible outside the scope of the conditional.

Contrived code example:

if eligible?
  bonus = 100
else
  bonus = 0
end
score + bonus

In that case, I would expect code to flow down one block, which will define bonus, or the other, which will define bonus.

I had forgotten about this WAT. To be honest, even reading the spec, I don't know that I would have expected non-executed code to have defined the variable.

Having said that, if you need to check later that a section of code was executed, checking against a variable being defined is surely setting a landmine for later programmers.

Let's check some history...

if condition
  foo = 42
end
...
if defined?(foo)
...

v2:

if condition
  foo = 42
end
...

if condition2
  foo = 6 * 9
else
  foo = 0
end
...
if defined?(foo)
...

Just don't, unless you're implementing some sort of inspect or something...

Actions

Also available in: Atom PDF