Unexpected behaviour when using Pluto with StaticArrays

I don’t think this is a Pluto issue. Running this begin ... end block in a fresh REPL gives the same error:

using StaticArrays

begin
     xᵢ = collect(range(0, 10, length=10)) |> SVector{10}
     yᵢ = @SVector [i for i in xᵢ]
end

# ERROR: LoadError: UndefVarError: `xᵢ` not defined

Not sure if the details/nomenclature here are technically correct, but macros are expanded before actual code is run in the expression you are trying to evaluate. So the whole expression in the block is parsed first, then the macros get replaced by the expressions they generate, and only then the code will run.

The error happens in the second step when the @SVector macro is trying to generate the expression that takes the values of xᵢ and use them to create yᵢ. Since the code to define xᵢ has not been run yet (happens after macro expansion), this cannot work. When generating an @SVector from the comprehension above, it basically reads all the values directly from the vector in the expansion (so xᵢ) and pastes them into the new expression (see below). But xᵢ hasn’t been defined yet, so the values cannot be read at the time of macro expansion.

I wonder if technically the SVector macro could do something else and just use the variable name xᵢ when generating the expression for the new vector, but apparently this doesn’t work (?). It might be because e.g. the length of the vector would have to be inferred somehow, probably only possible with length(xᵢ) at which point you would loose type stability.

One solution would be to just use SVector{n} as in the line above, since you already know the length and don’t really need to fall back to the convenience constructor. This will allocate the array [... for i in xᵢ] first and then convert it, but that doesn’t sound like a performance bottleneck.

begin
    xᵢ = collect(range(0, 10, length=10)) |> SVector{10}
    yᵢ = SVector{10}([i for i in xᵢ])
end
`@macroexpand`

This is the @macroexpanded @SVector expression (after xᵢ has been successfully defined). The values from xᵢ are pasted literally in the vector:

let
    var"#3#f"(i) = begin
        i
    end
    (SVector){10}((tuple)(var"#3#f"(0.0), var"#3#f"(1.1111111111111112), var"#3#f"(2.2222222222222223), var"#3#f"(3.3333333333333335), var"#3#f"(4.444444444444445), var"#3#f"(5.555555555555555), var"#3#f"(6.666666666666667), var"#3#f"(7.777777777777778), var"#3#f"(8.88888888888889), var"#3#f"(10.0)))
end

If the expression would return this expression

(SVector){10}((tuple)(var"#3#f"(xᵢ[1]), var"#3#f"(xᵢ[2]), var"#3#f"(xᵢ[3]),  ... ))

It might work, but at the moment I don’t understand the implications of doing that.

2 Likes