Running Tests vs. Debugging gives different results

When I normally run tester.jl below that calls my function encode(), it fails with a stack trace. When I debug run it in VS Code, it runs fine and does not fail. I even get the answer I expect. I am using VScode 1.52.1 with Julia 1.5.3.

VScode info:
Version: 1.52.1 (user setup)
Commit: ea3859d4ba2f3e577a159bc91e3074c5d85c0523
Date: 2020-12-16T16:34:46.910Z
Electron: 9.3.5
Chrome: 83.0.4103.122
Node.js: 12.14.1
V8: 8.3.110.13-electron.0
OS: Windows_NT x64 10.0.18363

# run-length-encoding.jl
function encode(str)
    # Run-length encoding (RLE)
    rle = "";

    n = length(str);

    for i in 1:n
        if i==1 # setup on the first iteration
            c = str[1];
            runLength = 1;
        elseif  c == str[i]
            runLength +=1;
        else
            # Record current run
            rle = runLength==1 ? string(rle, c) : string(rle, runLength, c);

            # restart run
            c = str[i];
            runLength = 1;
        end
        # Handle the end of the string
        if i==n
            rle = runLength==1 ? string(rle, c) : string(rle, runLength, c);
        end
    end

    return rle;
end
# tester.jl
include("run-length-encoding.jl");
encode("XYZ")

Stack Trace:

ERROR: LoadError: UndefVarError: c not defined
Stacktrace:
 [1] encode(::String) at c:\Users\jason\Exercism\julia\run-length-encoding\run-length-encoding.jl:11
 [2] top-level scope at c:\Users\jason\Exercism\julia\run-length-encoding\tester.jl:2
 [3] include_string(::Function, ::Module, ::String, ::String) at .\loading.jl:1088
 [4] include_string(::Module, ::String, ::String) at .\loading.jl:1096
 [5] invokelatest(::Any, ::Any, ::Vararg{Any,N} where N; kwargs::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}) at .\essentials.jl:710
 [6] invokelatest(::Any, ::Any, ::Vararg{Any,N} where N) at .\essentials.jl:709
 [7] inlineeval(::Module, ::String, ::Int64, ::Int64, ::String; softscope::Bool) at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\eval.jl:185    
 [8] (::VSCodeServer.var"#61#65"{String,Int64,Int64,String,Module,Bool,VSCodeServer.ReplRunCodeRequestParams})() at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\eval.jl:144
 [9] withpath(::VSCodeServer.var"#61#65"{String,Int64,Int64,String,Module,Bool,VSCodeServer.ReplRunCodeRequestParams}, ::String) at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\repl.jl:124
 [10] (::VSCodeServer.var"#60#64"{String,Int64,Int64,String,Module,Bool,Bool,VSCodeServer.ReplRunCodeRequestParams})() at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\eval.jl:142
 [11] hideprompt(::VSCodeServer.var"#60#64"{String,Int64,Int64,String,Module,Bool,Bool,VSCodeServer.ReplRunCodeRequestParams}) at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\repl.jl:36
 [12] (::VSCodeServer.var"#59#63"{String,Int64,Int64,String,Module,Bool,Bool,VSCodeServer.ReplRunCodeRequestParams})() at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\eval.jl:110
 [13] with_logstate(::Function, ::Any) at .\logging.jl:408
 [14] with_logger at .\logging.jl:514 [inlined]
 [15] (::VSCodeServer.var"#58#62"{VSCodeServer.ReplRunCodeRequestParams})() at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\eval.jl:109
 [16] #invokelatest#1 at .\essentials.jl:710 [inlined]
 [17] invokelatest(::Any) at .\essentials.jl:709
 [18] macro expansion at c:\Users\jason\.vscode\extensions\julialang.language-julia-1.0.10\scripts\packages\VSCodeServer\src\eval.jl:27 [inlined]
 [19] (::VSCodeServer.var"#56#57")() at .\task.jl:356
in expression starting at c:\Users\jason\Exercism\julia\run-length-encoding\tester.jl:2

My hunch is that c’s scope and definition are confined to the for-loop. In the debug run, c is defined just fine. In a normal run, c probably is optimized away, or somehow the for loop scope causes it not to be defined. Anyway, I think there is some bad practice here that I am violating but am a noob and still learning.

Thank you for explaining what is going on to a new Julia user.

When you are in a loop, think that all the variables created in the loop (and the iterator if it’s a for loop) are erased after each iteration. So, c does not exist in the second and later iterations when you call it in the elseif.

It could happen that when you were debugging you had defined another variable c in the global scope, and the loop is using it, maybe?

Thanks for replying, @heliosdrm. :slight_smile:

No, there is nothing in the global space. I have restarted VScode and also ran this in Juno. The results are similar. The code runs in debug mode and doesn’t run in normal mode.

# stack trace in Juno using a Julia REPL in a normal run
julia> include("tester.jl")
ERROR: LoadError: UndefVarError: c not defined
Stacktrace:
 [1] encode(::String) at C:\Users\jason\Exercism\julia\run-length-encoding\run-length-encoding.jl:9
 [2] top-level scope at C:\Users\jason\Exercism\julia\run-length-encoding\tester.jl:2
 [3] include(::String) at .\client.jl:457
 [4] top-level scope at none:1
in expression starting at C:\Users\jason\Exercism\julia\run-length-encoding\tester.jl:2

I think I need to understand the scope of variables inside of a for a loop.

Normally it’s as I told before: variables assigned inside a loop (such as c = ...) are local for each iteration (and the iterator of for loops is local as well), unless explicitly declared global (or in the REPL since 1.5, if there is a global with the same name). But you are right, in debug mode that rule does not hold. Not only in VS Code, also with Debugger.jl:

(after adding a breakpoint in line 9:)

julia> @run encode("XYZ")
Hit breakpoint:
In encode(str) at /home/meliana/prueba/run-length-encoding.jl:2
  5  
  6  n = length(str);
  7  
  8  for i in 1:n
> 9      if i==1 # setup on the first iteration
 10          c = str[1];
 11          runLength = 1;
 12      elseif  c == str[i]
 13          runLength +=1;

About to run: (==)(1, 1)
1|debug> fr
[1] encode(str) at /home/meliana/prueba/run-length-encoding.jl:2
  | str::String = "XYZ"
  | rle::String = ""
  | n::Int64 = 3
  | ::Tuple{Int64,Int64} = (1, 1)
  | i::Int64 = 1
1|debug> c
Hit breakpoint:
In encode(str) at /home/meliana/prueba/run-length-encoding.jl:2
  5  
  6  n = length(str);
  7  
  8  for i in 1:n
> 9      if i==1 # setup on the first iteration
 10          c = str[1];
 11          runLength = 1;
 12      elseif  c == str[i]
 13          runLength +=1;

About to run: (==)(2, 1)
1|debug> fr
[1] encode(str) at /home/meliana/prueba/run-length-encoding.jl:2
  | str::String = "XYZ"
  | rle::String = ""
  | n::Int64 = 3
  | ::Tuple{Int64,Int64} = (2, 2)
  | i::Int64 = 2
  | c::Char = 'X'
  | runLength::Int64 = 1

c exists at the outset of the second iteration, with the value that was assigned in the first one. Bug or feature?

1 Like

Thank you for the explanation!

I am not sure if it is a bug or a feature. For now, it is best for me to understand how it works and then fix it. I feel like too much of a noob to declare this a bug.

@heliosdrm, thank you for your help. The main issue is c and runLength were not visible from iteration to iteration in a normal run but were visible in a debug run. Defining c and runLength before the loop fixes the problem.

function encode(str)
    # Run-length encoding (RLE)
    rle = "";

    n = length(str);

    # Variables that are defined, so they persist from iteration to iteration. 
    c = "";
    runLength = NaN;
    for i in 1:n
        if i==1 # setup on the first iteration
            c = str[1];
            runLength = 1;
        elseif  c == str[i]
            runLength +=1;
        else
            # Record current run
            rle = runLength==1 ? string(rle, c) : string(rle, runLength, c);

            # restart run
            c = str[i];
            runLength = 1;
        end
        # Handle the end of the string
        if i==n
            rle = runLength==1 ? string(rle, c) : string(rle, runLength, c);
        end
    end

    return rle;
end

The relevant documentation is Scope of Variables. To summarize, the two different behaviors are a feature, and it must be understood why by reading the linked page. It is a mandatory read for any knew Julia user. This whole thread was my lesson in the scope of variables.

A short example demonstrating the lesson learned is below. c is not defined after the first iteration in running f() below. c is part of the local scope of the for-loop.

julia> function f()
           for i = 1:4
               if i==1
                   c = 1;
               end
               println("i=$i, Is c defined? Answer: $(@isdefined(c))")
           end
       end
f (generic function with 1 method)

julia> f()
i=1, Is c defined? Answer: true
i=2, Is c defined? Answer: false
i=3, Is c defined? Answer: false
i=4, Is c defined? Answer: false

The fix is to define the variable before the for-loop so it is part of the global scope. Running f2() below shows the result.

julia> function f2()
           c = 0;
           for i = 1:4
               if i==1
                   c = 1;
               end
               answer = @isdefined(c)
               print("i=$i, Is c defined? Answer: $answer.")
               if answer
                   print(" c=$c.\n");
               end
           end
       end
f2 (generic function with 1 method)

julia> f2()
i=1, Is c defined? Answer: true. c=1.
i=2, Is c defined? Answer: true. c=1.
i=3, Is c defined? Answer: true. c=1.
i=4, Is c defined? Answer: true. c=1.

I’m not that sure of this being really a feature in the case of the debugger. So I filed this issue:

https://github.com/JuliaDebug/Debugger.jl/issues/282

1 Like

@heliosdrm, thanks for filing an issue on Github. I don’t have the confidence yet to say it is a bug.