A slight confusion with a `let` block

Playing around the REPL, I got confused by the following (Julia 1.11):

julia> using StaticArrays

julia> L = 3;

julia> let L = 5, Q = @MVector [false for i in 1:L]
       @show L, Q
       end
(L, Q) = (5, Bool[0, 0, 0])
(5, Bool[0, 0, 0])

julia> let L = 5, Q = falses(L)
       @show L, Q
       end
(L, Q) = (5, Bool[0, 0, 0, 0, 0])
(5, Bool[0, 0, 0, 0, 0])

Essentially, the macro in the let block used the global value, and the non-macro assignment used the newly assigned L.
Is this confusing or expected? Any pointer to a previous discussion?

1 Like

interesting, looks like it’s something specific to MVector:

julia> L = 3;

julia> let L=5, M = @show L
       @show L, M
       end;
L = 5
(L, M) = (5, 5)

julia> L
3

digging deeper:

julia> @macroexpand @show L
quote
    Base.println("L = ", Base.repr(begin
                #= show.jl:1229 =#
                local var"#34#value" = L
            end))
    var"#34#value"
end

julia> @macroexpand @MVector [false for i in 1:L]
quote
    #= /home/akako/.julia/packages/StaticArrays/9Yt0H/src/SVector.jl:65 =#
    let
        #= /home/akako/.julia/packages/StaticArrays/9Yt0H/src/SVector.jl:66 =#
        var"#35#f"(i) = begin
                #= /home/akako/.julia/packages/StaticArrays/9Yt0H/src/SVector.jl:66 =#
                false
            end
        #= /home/akako/.julia/packages/StaticArrays/9Yt0H/src/SVector.jl:67 =#
        (MVector){3}((tuple)(var"#35#f"(1), var"#35#f"(2), var"#35#f"(3)))
    end
end

it looks like @show would preserve the local var_blah = L and return this local var_blah where in @MVector it got eagerly evaluated to 3

2 Likes

Yep, an equivalent result to falses(L) can be done by removing the macro call:

julia> let L = 5, Q = [false for i in 1:L]
       @show L, Q
       end
(L, Q) = (5, Bool[0, 0, 0, 0, 0])
(5, Bool[0, 0, 0, 0, 0])

This is what the MVector macro does:

macro MVector(ex)
    static_vector_gen(MVector, ex, __module__)
end

And this is what static_vector_gen does to :comprehension expressions like the example:

    elseif head === :comprehension
        #= omitted some input validation =#
        ex = ex.args[1]
        #= omitted some input validation =#
        rng = Core.eval(mod, ex.args[2].args[2])
        exprs = (:(f($j)) for j in rng)
        return quote
            let
                f($(esc(ex.args[2].args[1]))) = $(esc(ex.args[1]))
                $SV{$(length(rng))}($tuple($(exprs...)))
            end
        end

rng = Core.eval(mod, ex.args[2].args[2]) evaluates 1:L in the global scope because it needs a preexisting value while still parsing the let expression to put a number of arguments into the tuple call jling showed. This also causes problems if the global value would be instantiated too late, like if this were a begin block defining L before a @MVector call. This is documented behavior, per the docstring for @SArray:

 2. comprehensions
       │ Note
       │
       │ The range of a comprehension is evaluated at global scope by the macro, and must be made of
       │ combinations of literal values, functions, or global variables.

The most direct Github issue for local variables in comprehension ranges is #26, but it was closed in favor of the wider #97. The workaround given there is to skip the macro:

julia> let L = 5, Q = MVector{L}(false for i in 1:L)
       @show L, Q
       end
(L, Q) = (5, Bool[0, 0, 0, 0, 0])
(5, Bool[0, 0, 0, 0, 0])
3 Likes