[JuliaLowering] Implement static parameter capture - #62374
Merged
Conversation
mlechu
force-pushed
the
jl-sparam-capture
branch
2 times, most recently
from
July 14, 2026 00:04
af68d22 to
a0a065e
Compare
Member
|
Counter-examples, courtesy of you-know-who: # 1. Bound references the loop variable → UndefVarError at load time
for x in (Int, Float64)
global f
f(y::T) where {T<:x} = T
end
# flisp: works, defines both methods. This PR: UndefVarError(:x)
# 2. Bound references a global reassigned in the loop → silently stale bound
global b = Int
for i in 1:2
global g, b
g(y::T) where {T<:b} = (i, T)
b = Float64
end
g(1.5) # flisp: (2, Float64). This PR: MethodError (bound stuck at Int)seems that TypeVars can end up hoisted even when the Method def they accompany is not |
topolarity
reviewed
Jul 14, 2026
topolarity
left a comment
Member
There was a problem hiding this comment.
A few corners to solve, but largely this change looks quite good to me.
Nice that we were able to recover quite a bit of parity with flisp AFAICT, despite the difference in implementation.
| b = get_binding(ctx, bid) | ||
| if b.kind === :typevar # only visible to lifted scopes | ||
| ctx.scopes[ctx.scope_stack[end]].is_lifted || continue | ||
| end |
Member
There was a problem hiding this comment.
semi-exotic case:
global TE = Int
fE(x, y=((z::TE)->z)) where TE = y
fE(1.)(2)prints 2 in flisp
Member
Author
There was a problem hiding this comment.
Changed to match flisp, though I think this behaviour probably isn't what we want. Left a comment above the test case.
Member
There was a problem hiding this comment.
Agree w.r.t. questionable-ness
mlechu
force-pushed
the
jl-sparam-capture
branch
from
July 15, 2026 01:55
a0a065e to
27e0cca
Compare
topolarity
reviewed
Jul 15, 2026
topolarity
approved these changes
Jul 15, 2026
topolarity
left a comment
Member
There was a problem hiding this comment.
Nice work as usual @mlechu
aviatesk
added a commit
to aviatesk/JETLS.jl
that referenced
this pull request
Jul 17, 2026
JuliaLowering’s static-parameter capture changes (JuliaLang/julia#62374) method-definition lowering and introduces distinct `:typevar` bindings. Without matching updates, closure rewriting assumes the old method shape, diagnostics use obsolete context fields, and binding-based language features can miss or misclassify method type parameters. Update JuliaSyntax and JuliaLowering to `8399119aa7`, adapt `Closure2Opaque` to the separated typevar setup and method body, and use `sp_typevars` and `tv_deps` for static-parameter diagnostics. Handle `:typevar` bindings across cursor selection, completion, hover, semantic tokens, and occurrence analysis, preferring the typevar binding at a shared `where` binder source. Generic and generated local methods remain on the conservative synthetic-closure path. TypeAnnotation now covers outer static-parameter capture at closure call sites, while signature-view body inference still cannot recover static-parameter bounds from thunk method instances. Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
topolarity
added a commit
to topolarity/julia
that referenced
this pull request
Jul 17, 2026
Upstream's static-parameter capture redesign (JuliaLang#62374) collapsed the two top-level sequencing notions in closure conversion into a single flag that remains set inside loop bodies. Closure type definitions and method statements consequently stayed inside top-level loops, so every iteration re-ran `Core._structtype` and rebound the closure type name to a fresh DataType: any downstream code specialized on the previous iteration's type was invalidated and recompiled, an O(iterations) recompilation storm. ReverseDiff, whose array-derivative tables evaluate comprehension closures inside a ~200-iteration top-level `@eval` loop, paid 9.7x on precompile (126s vs flisp's 13s). Restore the distinction: statement-sequence constructs (if, elseif, block, try) preserve top-level sequencing, while loops reset it, so closure types and their methods hoist to before the loop -- defined once per site, exactly as flisp lowers them -- and only instance construction remains per-iteration. The typevar-setup hoist is gated to closures: a global method defined in a loop with a loop-dependent typevar bound must keep its signature evaluation in place. ReverseDiff returns to parity (14.4s, 1.06x flisp); per-iteration capture semantics are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Static parameter capture is (as far as I know) the last non-trivial, non-performance piece of
JuliaLowering that doesn't match flisp yet. I kind of understand why it was
left unimplemented. In flisp lowering:
done in desugaring
body, and the method-signature typevar at top level
somthing to the method definition's typevar list
inner lambda's sparams as well as the inner method definition's typevar list
parameters to feed into the
apply_typecalldesugaring's decisions, so:
core svecexpression is re-constructed to contain sparam captures in closure
conversion
analyze-vars-lambdamutates the lambda's sparam listStatic param capture into the lambda body in JuliaLowering happens to work today
because it's treated the same as a never-boxed local, so it's just a struct
field. I assumed this might cause inference issues, but admittedly didn't check.
The main issue is that capture into signatures doesn't work
(JuliaLang/JuliaLowering.jl#140).
This PR:
parts of this harder.
methodform out of desugaring to be(method argtype_svec lambda)and fully construct the svec after its contents are knownmethod_defsform to be(method_defs id typevar_list body),because typevar_list should always precede lifted stuff in closure conversion,
and because special resolution needs to happen when each
(typevar a b)getsresolved (resolve b first, then declare a).
were previously declared as locals, which had the wrong scope resolution
(they could all see each other), and this was a pkgeval problem.
:typevar. This behaves like a local up untilbefore linearization, and more like an ssavalue after that.
so that inner method signatures may resolve successfully (and even re-use the
original typevar)
This change also implements "underscore sparams are readable" #60626 and ports
#57928 (sparam capture in opaque closure is more like capture of locals). It
does not fix the pkgeval bug @topolarity reported to me with same-named
functions that require different closure types.
I've also added a cleaned-up Claude fix for typevars that depend on each other
in their bounds. Not sure if it's the best way of doing this. There's an easy
way out if this approach doesn't work (flisp just creates typevars
<:Anyinclosures).
The real change isn't as big as the diff implies, it just changes the closure
type creation near the beginning of the IR in many tests, so ends up changing a
lot of IR numbering.