Nested expressions rules seem different when `$` involved

So normally when nesting an expression into an expression, it seems like it doesn’t matter if the latter is written as a quoted :( ___ ) or an Expr(:quote, ___ ). There’s a difference when the former is written as a quote or an Expr, but the nested expressions eval to the same inner expression.

julia> @macroexpand x + 1
:(x + 1)

julia> :( :(x+1) ) == Expr(:quote, :(x+1) )
true
julia> eval( :( :(x+1) ) )
:(x + 1)

julia> :( :(x+1) ) == :( Expr(:call, :+, :x, 1) )
false
julia> eval( :( Expr(:call, :+, :x, 1) ) )
:(x + 1)

But things seem to change when the symbol $ is involved. First, there’s no way to quote the expression Expr(:$, :x) parsed from the source code $x, which kind of makes sense because $x is a syntax error. Second, putting Expr(:$, :x) in either a quoted :( ___ ) or an Expr(:quote, ___ ) now makes a difference. The former is more like a nested expression because it evals to the inner Expr(:$, :x). The latter is almost what happens in @eval $x (and why I started on this train of thought). Third, the latter is somehow equivalent to nesting a different expression :($x) in a :( ___ ). I can’t really make sense of this.

julia> x = 3
3
julia> @macroexpand $x 
:($(Expr(:$, :x)))
julia> Expr(:$, :x) # REPL print is redundant
:($(Expr(:$, :x)))

julia> :( Expr(:$, :x) ) == Expr(:quote, Expr(:$, :x) ) # ?!
false
julia> eval( :( Expr(:$, :x) ) )
:($(Expr(:$, :x)))
julia> eval( Expr(:quote, Expr(:$, :x) ) )
3

julia> Expr(:quote, Expr(:$, :x) ) == :( :($x) ) # ?!
true

I haven’t looked into this particular example too deeply, but probably

https://github.com/JuliaLang/julia/pull/45426

and

https://github.com/JuliaLang/julia/issues/43054

are relevant. The interpolation part you encounter is the remaining edge case jeff mentions in that issue…

1 Like

I believe there is a distinction between :( Expr(:$, :x) ) and Expr(:quote, Expr(:$, :x) )

For a simpler example, consider we have x=1 and compare :(x+1) and Expr(:quote, x+1).
the x+1 in :(x+1) is just a random string of letters; The compiler(or parser, for that matter) only interprets the meaning when :(x+1) gets evaluated.

x+1 in Expr(:quote, x+1), on the other hand, is computed immediately before we eval() it.
As a result, Expr(:quote, x+1) equals Expr(:quote, 2)

Similarly, when we do :( Expr(:$, :x) ) |> eval, the parser takes the string “Expr(:$, :x)”
, converts it to Expr object and evaluates the Expr object to get the final result, which is Expr(:dollar, :x) itself. This is what occurs when we type Expr(:dollar, :x) in the REPL and the ENTER key.

Howerver in Expr(:quote, Expr(:$, :x) ) , the Expr(:$, :x) is created (before eval() ever occurs). When we use Expr(:quote, Expr(:$, :x) ) |> eval, the parser attempts to evaluate the previously constructed Expr object, Expr(:$, :x),and the result of doing that is to refer the variable x. I believe extracting ___ from Expr(:$, :(___) would be a useful mental modelling of this action.

So rather than Expr(:quote, Expr(:$, :x)) I believe one possible equivalent form for :( Expr(:$, :x) ) is Expr(:quote, Expr(:$, :(Expr(:$, :x)))) .

Expr(:quote, Expr(:$, :x)) corresponds to quote; x; end, rather than quote; Expr(:$, :x); end or :(Expr(:$, :x))

I said “equivalent”, rather than “equal”, because for example :(x+1) != quote; x+1; end but they both mean the same thing. But these are just my thoughts.