Difference in behaviour between REPL and debugger

I am encountering an issue with Julia where code will run correctly in the debugger but throws an error when called from the REPL, which I don’t understand. A simple example of this is the code below:

function test()
    i = 0
    while i <= 1
        if i > 0
            var += 1
        end
        i += 1
        var = 1
    end
end

If I run @run test() then the function runs successfully, whereas if I just run test() I get the error

julia> test()
ERROR: UndefVarError: `var` not defined
Stacktrace:
 [1] test()
   @ Main ~/Code/asqp/temp_delete.jl:6
 [2] top-level scope
   @ REPL[1]:1

The error indicates that the variable var doesn’t exist, but it should be initialized on the first run through the while loop (the condition if i < 0 will fail on the first iteration) before it is referenced.

Could anyone please explain why I am observing this behavior?

The compiler is correct that var isn’t defined when it’s encountered in the code. The debugger is probably more forgiving due to its interpretative nature. Adding a local var solves the issue.

Thanks for the reply. I assume the it’s something to do with the scope in the while loop and it being different between the debugger and REPL, but am interested if someone could explain why.

I’m a little unclear on what the debugger is doing here but this sounds like a bug in its interpreter. var is local to each iteration of the while loop, so any time the body of the if block executes it should be an undef var error.

If the debugger is incorrectly modeling the scoping rules and just lazily adding variables whenever it encounters a new one, the execution makes sense. In that (wrong) semantic, the if is never entered so a non-existent binding is never read from (and thus no UndefVarError is thrown). Instead, var is just set to 1 later.

EDIT: Hm, this could be argued either way, so that either the compiler or the debugger are in the wrong here. If the debugger version is correct, the compiler would have to prove that in every possible execution trace, var is initialized before it’s read from in order not to throw an UndefVarError. If the compiler is correct here, the debugger would have to explore paths more eagerly and check whether it could be the case that the variable is read from before it’s initialized, irrespective of the actual values.

Either way, something is buggy.

1 Like

It does on the 2nd i=1 iteration, that’s when the compiled code throws an error. I think this is an unambiguous debugger issue because “In loops and comprehensions, new variables introduced in their body scopes are freshly allocated for each loop iteration.” var in the 1st i=0 iteration and its assignment are thus unrelated to var in the 2nd i=1 iteration; this has important implications for closures capturing iteration-local variables. To make a variable persist across iterations, it needs to exist in the scope the loop is in, like i. I don’t know how the debugger’s interpreter works, but would it be difficult to do a variable declaration pass* then toss out loop-local variables every iteration?

julia> function test()
           i = 0
           while i <= 1
               @show i
               if i > 0
                   print("if i > 0 reached: ")
                   var += 1
                   @show i, var
               end
               i += 1
               var = 1
               @show i, var
           end
       end
test (generic function with 1 method)

julia> test()
i = 0
(i, var) = (1, 1)
i = 1
if i > 0 reached: ERROR: UndefVarError: `var` not defined
...
julia> function test2()
           i = 0
           local var # even without a value it's good enough
           while i <= 1
               @show i
               if i > 0
                   print("if i > 0 reached: ")
                   var += 1
                   @show i, var
               end
               i += 1
               var = 1
               @show i, var
           end
       end
test2 (generic function with 1 method)

julia> test2()
i = 0
(i, var) = (1, 1)
i = 1
if i > 0 reached: (i, var) = (1, 2)
(i, var) = (2, 1)

*Funnily, you are allowed to put the local var declaration at the end because variable declarations are not executed at runtime, the parser or lowerer just puts them in scopes. If you write an assignment there instead, like var=10, that also serves to declare a variable in the scope but it will also instantiate a value at runtime that is returned by the method. Whether part of an assignment or not, declarations not adhering to the runtime order require a pass.

Thanks for the responses. Where should I go from here then? Should I raise it as an issue on the debugger github page?