-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: make macros bind tighter than commas #36547
Comments
c.c. @c42f, I remember discussing this with you on Slack, but that conversation is now lost beyond the Slack horizon. |
I agree this would be great, thanks for taking the time to capture this into an issue. As far as I'm aware there's no compelling reason for the current parsing, and I haven't personally seen anyone relying on it in the wild. |
It's a pity that |
Good points. I think anything that currently is delimited by parens should be unchanged, but I do think |
I'd really like this, too. The place where I'd foresee problems is multi-line DSL macros, since the comma could be seen as a way to split macro args over multiple lines without
|
I suspect this is too breaking to be done in a 1.x release, but I've marked for consideration in 2.0. |
Hypothetically, if I or someone else mocked up a PR and we ran PkgEval and showed that it broke no testsuites, would we still consider this too breaking for 1.x just because of the danger to user code? I can see myself being convinced either way that this either is or isn't acceptable for 1.x even if no testsuites broke. |
If it passes PkgEval, that's strong evidence that it's not likely to break user code. In that case we'd consider it. |
Using should be approximately the right way to try this out. I ran this change across In any case, it exposed a few interesting uses. One particular package which this would break common usage is https://github.com/mauro3/UnPack.jl eg, the usage in the readme @unpack a, b = pa |
Thanks for checking that out @c42f. Disappointing, but makes sense. |
A more subtle way to fix this could, perhaps, be to turn this on only when we're in a context where commas are already special. For example, within parentheses. So
This context-sensitivity might seem bad, but it already exists in the language in precisely this kind of way because
|
🤯 wow I never considered that. |
So thinking more about this, since we have However, this currently does cause problems if used outside of a call, so I think implementing #48738 would solve my concerns. |
Actually, nevermind, I think I do actually want what you're describing here @c42f because a macro can't tell the difference between |
I prototyped this more restricted approach. Applying it to # base/array.jl
iterate(A::Array, i=1) = (@inline; (i % UInt) - 1 < length(A) ? (@inbounds A[i], i + 1) : nothing)
# ^^^^^^^^^^^^^^^^^^^^^
# base/dict.jl
vals = T <: KeySet ? v.dict.keys : v.dict.vals
(@inbounds vals[i], i == typemax(Int) ? 0 : i+1)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# base/strings/basic.jl
IndexStyle(::Type{<:CodeUnits}) = IndexLinear()
@inline iterate(s::CodeUnits, i=1) = (i % UInt) - 1 < length(s) ? (@inbounds s[i], i + 1) : nothing
# ^^^^^^^^^^^^^^^^^^^^^ Conversely in the tests, there's the following cases which look intentional but could easily and more clearly be rewritten to use parentheses # test/bitarray.jl
r1 = func(args...; kwargs...)
ret_type ≢ nothing && (@test isa(r1, ret_type) || @show ret_type, typeof(r1))
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# test/fastmath.jl
for T in (Float32,Float64)
for func in (@fastmath exp2,exp,exp10)
# ^^^^^^^^^^^^^^^^^^^^^^^^
@test func(T(2000)) == T(Inf)
# test/llvmcall2.jl
@test (@eval ccall("llvm.floor.f64", llvmcall, Float64, (Float64,), 0.0)) === 0.0
# vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
@test (@eval ccall("llvm.floor", llvmcall, Float64, (Float64,), 0.0),
ccall("llvm.floor", llvmcall, Float32, (Float32,), 0.0)) === (0.0, 0.0f0)
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@test_throws err @eval ccall("llvm.floor.f64", llvmcall, Float32, (Float64,), 0.0) |
Hmm, interesting. Those usages in the tests do seem to indicate to me that the more restrictive form of this could be breaking, but I guess we'd have to look out in the wider ecosystem to know for sure. |
I ran JuliaSyntax over my copy of General with the rule that the scope only Overall, I'd say these results suggest that this syntax doesn't work like I've attached the full log of syntax mismatches here pkg_log_36547.txt. (Note that about 10 are unrelated bugs/differences in JuliaSyntax.) Possible bugs, which this change would "un-break"# ClimateMachine_0.1.0/src/Arrays/MPIStateArrays.jl
weighted_dot_impl(Q1, Q2, W) = dot_impl(@~ @. W * Q1, Q2)
#------------------------<
# Oceananigans_0.69.2/examples/kelvin_helmholtz_instability.jl
Ri_plot = plot(@. Ri * sech(zF / h)^2 / sech(zF)^2, zF; xlabel="Ri(z)", color=:black, kwargs...) # Ri(z)= ∂_z B / (∂_z U)²; derivatives computed by hand
#------------------------------------------------------------------------------------<
# WebIO_0.8.16/test/render.jl
@eval struct $TypeBName end
TypeA, TypeB = (@eval $TypeAName, @eval $TypeBName)
#----------------------------------<
Non-breakingUnintentional but harmless scopemisleading scope for # AMDGPU_0.2.17/src/device/array.jl
if (i % UInt) - 1 < length(A)
(@inbounds A[i], i + 1)
#---------------------<
# Stuffing_0.8.3/src/qtree_functions.jl
for j in labelslen:-1:i+1
push!(itemlist, (@inbounds labels[i], @inbounds labels[j]) => spindex)
#----------------------------------------<
# Zygote_0.6.34/src/lib/grad.jl
back = MacroTools.@q _ -> ($__source__; nothing)
push!(blk.args, :(@inline Zygote._pullback(::Context, ::Core.Typeof($(esc(f))), args...) = $(esc(f))(args...), $back))
#--------------------------------------------------------------------------------------------------< BreakingUnnecessary parentheses# BridgeSDEInference_0.3.2/src/solvers/tsit5.jl
function tsit5(f, t, y, dt, P, tableau)
#------------------------------------------------------------------------->
(@unpack c₁,c₂,c₃,c₄,c₅,c₆,a₂₁,a₃₁,a₃₂,a₄₁,a₄₂,a₄₃,a₅₁,a₅₂,a₅₃,a₅₄,a₆₁,a₆₂,
a₆₃,a₆₄,a₆₅,a₇₁,a₇₂,a₇₃,a₇₄,a₇₅,a₇₆ = tableau)
#---------------------------------------------------------< Bizarre styleA few people seem to looove short form function syntax and short-form block notation and will put up with writing piles of extra semicolons to use it. # ConstrainedRootSolvers_0.1.3/src/find_zero.jl
) where {FT<:AbstractFloat} =
#>
(
# _count iterations
_count = 0;
# define the initial step
@unpack x_ini, x_max, x_min, Δ_ini = ms;
# initialize the y
_tar_x = x_ini;
_tar_y = abs( f(_tar_x) );
# record the history
if stepping
push!(ms.history, [_tar_x, _tar_y]);
end;
……………
# 3. if break
if if_break(tol, FT(0), _Δx, _tar_y, _count)
# record the history
if stepping
push!(ms.history, [_tar_x, _tar_y]);
end;
break
end;
# 4. if no update, then 10% the Δx
if _count_inc + _count_dec == 0
_Δx /= 10;
end;
end;
return _tar_x
)
#<
Valid but misleading# ConvexBodyProximityQueries_0.2.0/test/obstacles.jl
@test typeof(@point rand(3)) <: ConvexPolygon{3, 1}
@test typeof(@line rand(2), rand(2)) <: ConvexPolygon{2, 2}
#----------------------------<
# Compose_0.9.3/src/property.jl
svgattribute(attributes::AbstractArray, values::AbstractArray) =
#-------------------------------------------------->
SVGAttribute( @makeprimitives SVGAttributePrimitive,
(attribute in attributes, value in values),
SVGAttributePrimitive(attribute, string(value)))
#----------------------------------------------------------<
# MatrixNetworks_1.0.2/test/diffusions_test.jl
tol = 1e-3
x = pagerank_power!(x,y,P,0.85,v,tol,maxiter,(iter,x) -> @show iter, norm(x,1))
#-------------------------------------------------------------------------<
# ModelingToolkit_8.3.2/test/variable_parsing.jl
@test @macroexpand(@parameters x, y, z(t)) == @macroexpand(@parameters x y z(t))
#----------------------------------< Somewhat useful casesBut these can be fixed with parens # OrdinaryDiffEq_6.6.5/src/nordsieck_utils.jl
isconstcache = T <: OrdinaryDiffEqConstantCache
isconstcache || ( @unpack atmp, ratetmp = cache )
#-------------------------------<
# RRRMC_2.2.0/src/graphs/PercStep.jl
(e1-e0) == delta || (@show e1,e0,delta,e1-e0; error())
#--------------------------------< |
Darn, this does seem like a mistake. Maybe we could do a release with a warning for this, and eventually change it? Summary for triage: in |
It would also be nice to change the scoping in |
Agreed @LilithHafner, I'd like to include changing the parsing of So there's several possible proposals for making macro calls bind tighter than commas, ordered from least to most breaking:
We've already discarded (3) because it's very breaking and the natural appearance of I believe (2) is most consistent. But if too breaking, we could consider (1). I also think we can do syntax evolution in a less breaking way than relying on warnings and a deprecation schedule. But that's a whole other thing I shouldn't get carried away explaining on this issue ;-) |
Would you share a link or post more info on this elsewhere? Your work on syntax is very nice and I'd love to hear your ideas about this. |
From triage: Long term, implement option 2. Short term, warn whenever this would change parsing. We can implement the warning now, no need to wait for JuliaSyntax to merge. |
Excellent decision imo. Thanks triage ❤️ |
So how do we move forward with this? I looked around in |
@MasonProtter I've pushed my JuliaSyntax prototype implementation here for reference: JuliaLang/JuliaSyntax.jl#212. You can see there's a new Warnings are a little tricky in the flisp parser as there's no obvious place to communicate them. Maybe the simplest, least disruptive way to get them out of the scheme code would be to use a dynamically scoped list to collect warnings from the parsing functions. Then modify the entry points called from the C code to return the warnings as a separate output. Ah wait. I see this is what we're already doing now for lowering. So following that code should be easy enough: https://github.com/JuliaLang/julia/blob/master/src/jlfrontend.scm#L158 I'd like a way for JuliaSyntax to be able to communicate warnings in future, separately from the parse tree. Currently this isn't possible with |
By the way, I'm not super keen to mess with the scheme implementation anymore, I'd rather put that effort into landing JuliaSyntax. But I'm happy to support anyone else in making changes to the scheme code. And help with any changes to the Julia runtime which would benefit both parsers. |
Diagnostics design sketchOk. How about the following sketch for how diagnostics could flow through the parser and runtime:
Design alternative with
|
I've updated the code at JuliaLang/JuliaSyntax.jl#212 to add this behavior as a feature flag which can be toggled at runtime to make testing this easier. And added the code which tests against the General registry there. I also enabled this change within julia> parsestmt(SyntaxNode, "[a, @x b, c]", low_precedence_comma_in_brackets=true)
line:col│ tree │ file_name
1:1 │[vect]
1:2 │ a
1:5 │ [macrocall]
1:6 │ @x
1:8 │ b
1:11 │ c
julia> parsestmt(SyntaxNode, "[a, @x b, c]", low_precedence_comma_in_brackets=false)
line:col│ tree │ file_name
1:1 │[vect]
1:2 │ a
1:5 │ [macrocall]
1:6 │ @x
1:7 │ [tuple]
1:8 │ b
1:11 │ c Also, here's another example of a fun bug (I guess?!) in General due to confusion about the current parsing rules: ┌ Error: Parsers succeed but disagree
│ fpath = "BesselK_0.1.0/paperscripts/testing/matern_derivative_speed.jl"
│ failing_source =
│ const oo = @SVector ones(2)
│ const zz = @SVector zeros(2)
│ # ┌───────────────────────────
│ const TESTING_PARAMS = (@SVector [1.25, 0.25, 0.5],
│ @SVector [1.25, 5.25, 0.5],
│ @SVector [1.25, 0.25, 0.75],
│ @SVector [1.25, 5.25, 0.75],
│ @SVector [1.25, 0.25, 1.0],
│ @SVector [1.25, 5.25, 1.0],
│ @SVector [1.25, 0.25, 1.75],
│ @SVector [1.25, 5.25, 1.75],
│ @SVector [1.25, 0.25, 3.0],
│ @SVector [1.25, 5.25, 3.0],
│ @SVector [1.25, 0.25, 4.75],
│ @SVector [1.25, 5.25, 4.75])
│ #──────────────────────────────────────────────────┘
│
│ for pp in TESTING_PARAMS |
Amazing, thanks for working in this @c42f! I had spent some time digging into the scheme and was left suitably discouraged that I agree we should just focus on JuliaSyntax.jl as well. Your work on it is awesome, as usual. |
Thanks so much @MasonProtter 😊 I know you've got plenty of your own projects but if you ever feel interested in contributing over at JuliaSyntax there's lots to do :-) by the way I've copied the comment at #36547 (comment) out of this thread and over to JuliaLang/JuliaSyntax.jl#276 and will probably start working on something like that as part of the JuliaSyntax integration soon. |
Currently, we have that
@foo a, b, c
parses as@foo((a, b, c))
which seems sensible enough, but it's a major source of frustration when you have a macro inside a function's arguments. As a concrete example, consider a very simple underscore anonymous function macro that would take@_ f(_) + 1
and turn it intox -> f(x) + 1
. With the current parsing of macros, if we writethis gets parsed as
rather than the desired
I'm wondering if anyone has a feeling for how disruptive it would be to change this in 2.0 and if they feel that such a change would be desirable. I'm not sure I've ever seen someone write
@foo a, b, c
where they didn't intend it to mean(@foo(a), b, c)
, but this could be a reflection of my ignorance.Is there some deep and useful reason for the current behaviour that I'm missing?
The text was updated successfully, but these errors were encountered: