Does `@label`/`@goto` introduce scope like loop blocks?

Variables assigned in a loop block scope are different across iterations while if statements don’t introduce new local scope at all, but what’s the variable scoping behavior for @label-@goto jumps? Initially I’d assume they just jump in and out of scopes created by other blocks, like in this example of 2 closures capturing 1 variable we assign twice by jump:

julia> function foo()
         # 1 (steps for following control flow)
         closures = []
         count = 0
         # 2, 4
         @label assignx
         @show x = rand()
         if count == 1 @goto next end
         # 3
         push!(closures, () -> x)
         count += 1
         @goto assignx
         # 5
         @label next
         push!(closures, () -> x)
         closures
       end
foo (generic function with 1 method)

julia> result = foo(); dump.(result);
x = rand() = 0.7004247485201015
x = rand() = 0.8141380475376465
#87 (function of type var"#87#89")
  x: Core.Box
    contents: Float64 0.8141380475376465
#88 (function of type var"#88#90")
  x: Core.Box
    contents: Float64 0.8141380475376465

julia> result[1].x === result[2].x # same boxed, captured variable
true

But when I rearrange the jump to resemble a while loop, it appears to make separate local variables as if it’s a loop block scope, in other words making an assignment after a jump created a separate captured variable. On the other hand, this variable isn’t isolated to the “loop” body:

julia> function bar()
         # 1
         closures = []
         count = 0
         # 2, 3
         @label assignx
         @show x = rand()
         push!(closures, () -> x)
         count += 1
         if count == 1 @goto assignx end
         # 4
         closures, x
       end
bar (generic function with 1 method)

julia> result = bar(); dump.(result);
x = rand() = 0.1924894128912077
x = rand() = 0.6303097583847382
Array{Any}((2,))
  1: #27 (function of type var"#27#28"{Float64})
    x: Float64 0.1924894128912077
  2: #27 (function of type var"#27#28"{Float64})
    x: Float64 0.6303097583847382
Float64 0.6303097583847382

Not sure if it matters, but this exercise was an offshoot of finding out that @label (even with no @goto) causes boxing of captured variables even in the absence of reassignments. All ran in v1.11.5.

3 Likes

This makes some level of perverse sense, since loops do literally just lower down to goto and label statements:

julia> code_lowered(()) do
           count = 0
           while true
               count += 1
           end
           count
       end
1-element Vector{Core.CodeInfo}:
 CodeInfo(
1 ─      count = 0
2 ┄      goto #4 if not true
3 ─ %3 = Main.:+
│   %4 = count
│        count = (%3)(%4, 1)
└──      goto #2
4 ─ %7 = count
└──      return %7
)
julia> code_lowered(()) do
           count = 0
           @label loopstart
           count += 1
           @goto loopstart
           count
       end
1-element Vector{Core.CodeInfo}:
 CodeInfo(
1 ─      count = 0
2 ┄ %2 = Main.:+
│   %3 = count
│        count = (%2)(%3, 1)
│        nothing
└──      goto #2
3 ─ %7 = count
└──      return %7
)

Notice the only difference between those two forms is a junk goto #4 if not true in the loop case and a dangling nothing.

The compiler has to later on ‘rediscover’ the presence of loops in order to do various things, so I’m guessing you just tricked it into thinking a loop existed.

2 Likes

Maybe in a sense of whatever criteria it has for distinguishing variables of the same name, as there’s no equivalent Julia loop for either function. foo doesn’t behave like one already, but the second closure is instantiated outside its assignx area where x is assigned, which would be impossible across a block introducing local scope. bar allows access to x after its assignx area (amended example to demonstrate), which would be impossible for the same reason.

julia> let
         while true
           x = rand()
           break
         end
         x # no local x here
       end
ERROR: UndefVarError: `x` not defined in `Main`

Even besides that unsettling half-measure of bar’s scoping of x, @label and @goto don’t form a neat block to even suggest a local scope so this seems bad on a semantic level.

1 Like

I guess @c42f, author of the in-development package JuliaLowering.jl, might have an opinion?