[JuliaLowering] Syntax versioning and macro-expansion improvements#62221
Conversation
|
I'm mildly concerned about versioning of syntax being a bit brittle for "non-local" constructs (e.g. I assume we'll typically have a single head / node that corresponds to the construct and can mostly use that to choose lowering / macro behavior, which seems good enough - just something to watch out for composability-wise On the plus side, this does seem to handle interpreting a |
|
Changes just now:
|
|
I think the "manual macroexpand" case shouldn't be too hard to support (and I might do that now, since there are a lot of "cannot document" errors in the pkgeval). I probably want a unique context per top-level thunk, and to change "unknown layer" in Claude (welcome back Fable) also found some issues, which I'll fix:
|
|
I think this works now. I've made the changes in the previous comment, which cleaned up the semantics around macros that emit toplevel/module. Every top-level thunk gets one and only one base layer, and each macro expansion may produce arbitrary crap, but Macros should now be able to call JuliaLowering.include_string(test_mod, raw"""
macro undo_inert(x)
x2 = JuliaLowering.macroexpand(x)
x2[1]
end
""")
@test JuliaLowering.include_string(test_mod, raw"""
let foo = 1; @undo_inert(:foo); end
""") == 1
@test JuliaLowering.include_string(test_mod, raw"""
let foo = 1; @undo_inert(@legacy_quote_to_syntax(:foo)); end
""") == 1I suspect (but haven't tested) that repeated cc @Keno I've also added a julia> JuliaLowering.include_string(test_mod, raw"""
using JuliaLowering: @syntax_version, @legacy_quote_to_syntax
@syntax_version v"1.13" macro old_macro(args...)
esc(Expr(:tuple, "old", typeof(args), args...))
end
macro new_macro(args...)
@legacy_quote_to_syntax :("new", $(typeof(args)), $(args...),)
end
println(stdout, @old_macro(1, 1+1, :(1+1)))
println(stdout, @new_macro(1, 1+1, :(1+1)))
""")
("old", Tuple{Int64, Expr, Expr}, 1, 2, :(1 + 1))
("new", Tuple{SyntaxTree{Dict{Symbol, Dict{Int64, Any}}}, SyntaxTree{Dict{Symbol, Dict{Int64, Any}}}, SyntaxTree{Dict{Symbol, Dict{Int64, Any}}}}, 1, 2, :(1 + 1))of course, we still need to work on the interface for creating SyntaxTree, which for now is just |
|
Also fixed the regressiosn found in #61576: I'm using |
|
@mlechu What's the stance on: macro foo()
:(@eval const x = :foo)
endI don't believe the expected edit: Looks like |
|
One more comment: It occurs to me that this still leaves Expr vs. SyntaxTree incoherent w.r.t. their notion of self-quoting IR. I assume the intended fix here will be to make |
|
What problems arise if we just make |
7a3534e to
9e1da8c
Compare
Thanks, should be fixed now. I guess I took some tests from claude's attempt at topolarity@592451a |
I believe the main issue is anywhere we do
Sounds coherent to me (and mini-PkgEval agrees with you)
Nice call out. Yeah, apparently CBOOCall.jl relies on this (particularly the referent module): julia> JuliaLowering.include_string(Main, """
module Foo
macro mkconst(val)
Expr(:block, Expr(:const, Expr(:(=), :foo, esc(val))))
end
end
"""; expr_compat_mode=true)
julia> module Flisp; using ..Foo; Foo.@mkconst 13; end
julia> names(Flisp; all=true) .|> String .|> contains("foo") |> any
true
julia> isdefined(Foo, :foo)
falsecompared to JL: julia> JuliaLowering.include_string(Main, """
module JL; using ..Foo; Foo.@mkconst 13; end
"""; expr_compat_mode=true)
julia> names(JL; all=true) .|> String .|> contains("foo") |> any
false
julia> isdefined(Foo, :foo)
trueas a lowering outsider, it's a bit odd that the LHS of Anyway I guess this means more tweaks to our hygiene exception behavior? Doesn't need to be this PR |
I see. I'll keep it in mind; probably best to follow what Racket does here.
Not sure if a "good" reason, but |
topolarity
left a comment
There was a problem hiding this comment.
Various miscellaneous (minor) typos, etc. - many pointed out by Claude 🤖
First batch before a proper review from me
|
I don't think we have much of a chance to fix this one without eagerly applying hygiene on macro boundaries, which sounds gnarly: julia> macro oldpair(a, b)
:(($(esc(a)), $(esc(b))))
end
julia> JuliaLowering.include_string(Main, raw"""
macro mix(callerx)
JuliaLowering.@legacy_quote_to_syntax quote
x = "macro-layer x"
@oldpair $callerx x
end
end
let x = "caller x"; @mix x end
""")
("macro-layer x", "macro-layer x")but wanted to get your thoughts (and maybe add the test for doc) |
Good question. This looks like the I think it would be reasonable to throw an error if syntax is not at the expected layer in |
topolarity
left a comment
There was a problem hiding this comment.
Mentioned to @mlechu some concerns around the semantic impacts of the JL_OLD_SYNTAX_VERSION fallback for anything coming out of old-style macros. She had several ideas - most promising might be the observation that syntactic forms are all Expr (not leaf literals) so they reliably have identity and we can potentially track their provenance with a field or side table etc. That's potentially much better than our provenance situation for identifiers etc. which do not have identity and so cannot be reliably tracked.
Separate note, but I am sure we'll run into some quirks eventually w.r.t. the lossy-ness of apply_expansion_layer but I think our co-existence troubles with old-style macros probably dominate over the scope limitations here. This seems to chart a fairly clear path for lifting those limitations later too.
Anyway this change looks good to go from my POV. A few hygiene fixes to follow-up with, but nothing that blocks these (nice) changes. Thanks!
Co-authored-by: Cody Tapscott <topolarity@tapscott.me>
Co-authored-by: Cody Tapscott <topolarity@tapscott.me>
Claude loves doing this
Co-authored-by: Cody Tapscott <topolarity@tapscott.me>
|
https://buildkite.com/julialang/julia-pr/builds/260 was a full pass otherwise (the PR has not changed), so rather than waste more CI time let's merge this 👍 |
JuliaLang/julia#62221 changes JuliaLowering’s expansion API, syntax contexts, and inert syntax representation. Without matching updates, JETLS can lose source-aware bindings, mis-handle generated code, and resolve macro-generated names in the caller’s scope. Rebase source trees before expansion, derive binding layers from `SyntaxContext`, and handle `syntaxinert` throughout binding and occurrence analysis. Update the new-style Base macro stubs to preserve provenance while keeping generated locals hygienic and using explicit `Base`/`Core` references for fixed helpers. Evaluate nested `@static` conditions in the caller module and teach noreturn detection about explicit global references so definite-assignment analysis remains accurate. Co-Authored-By: GPT-5.6 Sol <noreply@openai.com>
JuliaLang/julia#62221 changes JuliaLowering’s expansion API, syntax contexts, and inert syntax representation. Without matching updates, JETLS can lose source-aware bindings, mis-handle generated code, and resolve macro-generated names in the caller’s scope. Rebase source trees before expansion, derive binding layers from `SyntaxContext`, and handle `syntaxinert` throughout binding and occurrence analysis. Update the new-style Base macro stubs to preserve provenance while keeping generated locals hygienic and using explicit `Base`/`Core` references for fixed helpers. Evaluate nested `@static` conditions in the caller module and teach noreturn detection about explicit global references so definite-assignment analysis remains accurate. Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
|
I merged aviatesk/JETLS.jl#799. This allows JuliaLang/JuliaLowering.jl#108 and aviatesk/JETLS.jl#108 to be closed. Once again, thank you for your work, @mlechu! |
Attach a new SyntaxContext to all syntax, which is shared between all nodes of
the same macro expansion (with some exceptions for
escape, etc.).Cool new stuff
Macros can now be syntax-version-aware, so the following is now possible:
and I've added the first one,
@legacy_quote_to_syntaxfor testing purposeswithin JuliaLowering:
Design
TL;DR: Racket does it right. The following should be equivalent:
SyntaxTreeExpr/Symbol/etcK"inert"/QuoteNode'()(quote)K"quote"/:()`()(quasiquote)K"$",()(unquote)syntaxinert(formerly inert_syntaxtree)#'()(syntax)syntaxquote(new, no surface syntax yet)#`()(quasisyntax)syntaxunquote(new, no surface syntax yet)#,()(unsyntax)This PR doesn't implement the full sets-of-scopes resolution, though it does
make it easier to implement if we wanted that. No local macros makes it
less important. We miss out on some cross-toplevel-thunk resolution, which I
think is OK or desirable for now.
After each macro expands, we want to apply a new scope layer to all new syntax
(that wasn't passed as a macro argument at some point). Racket applies the new
scope to all argument syntax, expands, then inverts that scope throughout the
expansion. This PR expands, then applies the new layer to all syntax whose
existing layer is not known to be "under" that new layer.
Fixes
Our integer-ID-based scope layer dies with the
MacroExpansionContextobject,so the return value of macro expansion isn't a self-contained thing you can
eval. Fix this.
Right now, when
expr_compat_mode=true, the surface syntax:(x + 1)is aSyntaxTree, but the
:xis still Symbol. That means we have a special caseto support the following:
and this causes IR validity errors:
This PR takes the same approach as Racket: Non-syntax interpolated into
syntax is automatically upgraded. (This automatic upgrade also happens with
the return value of a macro, and the body of a generated function, although
I've left a couple cases disallowed to catch my own mistakes for now).
On top of the above,
quote; endand:()no longer produceSyntaxTreeunder any circumstance. This PR uses
@legacy_quote_to_syntaxas astop-gap, which converts
quote/$tosyntaxquote/syntaxunquote. Ideally wecan add surface syntax for this later.
:no longer producesSyntaxTree, but that used to be one of a whole classof problems now solved by syntax carrying its version: An expansion may
contain arbitrary mixtures of syntax, so e.g. increased strictness on a form
in a newer version can't be enforced without changing the form (like in
syntax: Disallow using
@gototo skip finally blocks #60979).I got rid of
:scope_layerwhen calling old macros from new ones. This fixesBug report from JETLS:
:scope_layerinterfering with user code JuliaLowering.jl#108, which has beencausing some pkgeval and JETLS problems (cc @aviatesk), and clearly got in the
way of the robot in WIP: Separately compile JuliaSyntax/JuliaLowering into a standalone frontend #62073. I've fixed our premature collapse of
escapenodes in
prepare_macro_argsso we shouldn't need it, and in general, macrohygiene for old-style macros is not something that can be done without
breakage or drastic changes to Symbol.
Change my approach from [JuliaLowering] Implement macro hygiene exemptions for some forms #62013 for macro hygiene exemptions. This PR does it
in desugaring, which is closer to how macroexpand.scm does it.
Add
macroexpand1Fix modules-in-expansions-with-escaped-names (pkgeval)
Fix
$in dot-rhs (pkgeval)Fix some cases of invalid IR from generated functions returning weird forms
TODO
expr_compat_modeparameter should be deprecated in favour of the treecarrying its syntax version.
syntax_module(st)instead ofctx.modthroughout lowering? Probablymore correct, though may cause some "eval-into-closed-module" errors
context is mutated or updated immutably.
needs to be an iterator.
macroexpandPinging @Keno to take a look at this approach to syntax versioning: I'll
probably want to remove Core.MacroSource (and the current implementation of
@VERSION) if it works.@aviatesk This will likely require JETLS tweaks (e.g.
ex.context.layer === ctx.syntax_context.layerinstead ofex.scope_layer == 1); let me know if youneed anything on the JL end.
Some tests written with AI.