release: 2026-05-06#675
Merged
Merged
Conversation
This needs a more general fix.
TypeAnnotation: Resolve local closure types via `OpaqueClosure`
…tator" This reverts commit 596579a.
The `TypeAnnotation` module (ef22f68) routes local closures through `JL.convert_closures`'s synthetic-struct path: each closure becomes a method on a synthetic type (`var"#closure#N"`) hoisted to a toplevel `:method` 3-arg. Because `infer_toplevel_tree` deliberately skips `Core.eval` to keep analysis side-effect-free, that synthetic type never appears in `context_module`. CC can then resolve neither `getfield(self, …)` on the closure env nor dispatch the closure's call site, so the closure body's captures and the enclosing function's call to the closure both infer as `Any`. Fix this for the common case (single-method local closures) by routing them through CC's native `OpaqueClosure` path instead. OCs carry their `Method` directly on the value (no method-table lookup needed) and their env is a typed tuple, so CC infers both the body and the call site precisely without any `context_module` dependency. A new pre-lowering pass `rewrite_local_closures_to_opaque` (`src/analysis/closure-to-opaque.jl`) runs before `JL.convert_closures` and turns `K"function_decl"` + `K"method_defs"` pairs for single-method local bindings into `K"_opaque_closure"` form. A whole-tree pre-pass counts `K"method_defs"` per `var_id`, so multi-method bindings (which can't fit in one OC) are skipped and fall through to the existing synthetic-struct path. `ASTTypeAnnotator` carries an `oc_body_trees::IdDict{Method, SyntaxTreeC}` populated by `register_oc_body_trees!` after a thunk's `resolve_definition_effects_in_ir` resolves `:opaque_closure_method` Exprs to concrete `Method`s. `finishinfer!` then looks up `frame.linfo.def` to find the body's citree and annotate it. That annotation needs to be deterministic. CC infers an OC body once at the construction site — via the eager `abstract_call_opaque_closure(check=false)` call from `abstract_eval_new_opaque_closure`, using `most_general_argtypes(po.typ)` (i.e. the signature view) — and then again at each real call site, on the per-argtypes specialization. Without coordination every `finishinfer!` for the same `Method` would overwrite the citree's `:type` attributes, leaving the body annotation dependent on which call site CC processed last. We avoid this by marking the OC's `Method` in an `oc_methods_to_annotate` `Set` from the `abstract_eval_new_opaque_closure` override before the eager inference runs; `finishinfer!` then consumes the marker atomically (annotate + delete), so only the signature-view specialization writes annotations and per-call-site specializations find the marker gone and skip. The result is the same UX rule used for top-level method bodies: body annotations reflect the signature, call-site annotations reflect actual dispatch. That override carries one more refinement (orthogonal to the marker above): the OC's static rt parameter. It binds `PartialOpaque.typ`'s rt typevar to the eager body inference's result, so the OC type survives `widenconst` boundaries (e.g. `Base.@default_eltype`'s `Tuple{typeof(itr)}` widening inside `map`) without collapsing to `T<:rt_ub`. This refinement is IPO-unsound in general — `OpaqueClosure`'s rt parameter is fixed by `rt_ub` at construction, not by the body's actual return — and was rejected upstream (JuliaLang/julia#61718). It's contained here because `ASTTypeAnnotator` results never feed optimization, caching, or runtime; they only drive tooling, and the closure→OC rewrite already accepts a class of incompleteness. Tests in `test_closure_to_opaque.jl` exercise the rewrite end-to-end (parse → rewrite → lower → `Core.eval`). `test_TypeAnnotation.jl` adds closure precision tests, including a multi-call-site case that demonstrates the body annotation is deterministic (signature view) rather than tied to call site order. Box-erasure cases (reassigned/mutated captures, self-recursion) assert the documented `Any`, since they're inherent to JL's `Core.Box` strategy. The untyped `do x` `map` body asserts `Any` (signature view, matching native closure inference for the method body), but the result vector stays `@test_broken === Vector{Int}` — native closures dispatch through the synthetic struct's method table and infer it precisely; matching that here needs call-site-aware refinement on the OC's rt parameter. Multi-method local closures stay `@test_broken` because they fall through to the synthetic-struct path and lifting them needs sandbox-materialization on the JETLS side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Block-end inlay hint labels previously rendered as bare text (e.g. `end module Foo`) while the underlying `textEdit` inserts a `#= … =#` block comment (`end #= module Foo =#`). Bake the markers into the displayed label so the editor shows the same comment that gets inserted when the hint is applied. Also adds `apply_inlay_hints` (now honouring `paddingLeft` / `paddingRight`) and `get_syntactic_inlay_hints` helpers at the top of the test file, paving the way for end-to-end syntactic-hint tests. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) `handle_DocumentDiagnosticRequest` now derives a content-based `resultId` (shared with `workspace/diagnostic` via the renamed `compute_diagnostic_result_id`) and returns `RelatedUnchangedDocumentDiagnosticReport` when it matches the client's `previousResultId`, skipping diagnostic recomputation entirely. This reduces redundant work on clients that aggressively re-pull diagnostics across all open files on each edit (e.g., Zed). Also extracts `compute_pull_diagnostics` and `postprocess_pull_diagnostics` as helpers shared between `textDocument/diagnostic` and `workspace/diagnostic`, removing duplicated parsed-stream branching, config application, notebook localization, and markdown rendering. This consolidation lets us add mid-analysis cancellation handling to the workspace path as well. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…671) Adds two `@testset`s in `test/test_diagnostic.jl` exercising the pull diagnostic resultId handling end-to-end through the LSP request flow. `textDocument/diagnostic resultId lifecycle` covers: - initial pull returns a `RelatedFullDocumentDiagnosticReport` with a `resultId` - repeat pull with matching `previousResultId` returns `RelatedUnchangedDocumentDiagnosticReport` - editing the document via `didChange` invalidates `resultId` - mismatched `previousResultId` falls back to a full report `workspace/diagnostic resultId lifecycle` opens util.jl (not the main file) under a multi-file package with `using Base: sum` in the entry file, and covers: - synced files are skipped from the workspace report - the unsync'd main file carries the lowering `unused-import` diagnostic - repeat pull with matching `previousResultIds` returns unchanged - editing util.jl invalidates main.jl's `resultId` (cross-file hash via `compute_diagnostic_result_id`) and clears the unused-import on the next pull, without needing a full-analysis re-trigger - switching `diagnostic.all_files` to false suppresses the unsync'd file in the workspace report Also drops the unused `raw_res` destructuring in the `syntax error diagnostics` testset. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Introduce `HierarchicalTestSet` (in `test/HierarchicalTestSet.jl`), an `AbstractTestSet` that wraps a `DefaultTestSet` and overrides `record` to print the full testset path (`outer > middle > leaf`) on failure, instead of just the innermost description that `DefaultTestSet` shows. Specify on the outermost `@testset` only; nested ones inherit the type via Test.jl's `testsettype` propagation. The path renderer (`ts_description`) detects sibling `HierarchicalTestSet` instances by the unique `:__hierarchical_testset_inner__` field name rather than by type, because `HierarchicalTestSet.jl` is included by both `runtests.jl` (in `Main`) and each `test_XXX.jl` (inside `module test_XXX`), so the type ends up defined separately per module and a single dispatch wouldn't cover both. Other testsets are matched only via `Test.DefaultTestSet`'s public `description`; anything else (e.g. `TestRunner.TestRunnerTestSet`) falls back to the type name to keep the renderer crash-free without assuming non-public internals. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend `type_for_funcdef` to also walk `K"opaque_closure_method"` nodes. Single-method local closures rewritten to `OpaqueClosure` by `rewrite_local_closures_to_opaque` lower to `K"opaque_closure_method"` instead of `K"method"`, so the funcdef byte range previously yielded no return type even though the OC's `PartialOpaque` carried it. The two node kinds share the same `K"code_info"` child structure, so the existing return-statement walk applies as-is. This in turn lets inlay hints emit `function f(…)::T` / `f(…)::T = …` annotations for single-method local closures. Also rewraps the `get_type_for_range` docstring to fit the 92-char budget more tightly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipe the current editor buffer to the `testrunner` subprocess via stdin (using the new `--read-stdin` flag) instead of relying on the on-disk file. Drop the `get_saved_file_info` / sourcetext sync check in `testrunner_run_testset_from_uri` and `testrunner_run_testcase_from_uri`, so unsaved edits — including brand-new `@testset` / `@test` cases — execute as-is from code lens and code action. Plumb the buffer source through `_testrunner_run_testset` / `_testrunner_run_testcase` (extracting it from `fi.parsed_stream` for testset, and capturing it on the `TestRunnerTestcaseProgressCaller` for testcase) so it survives the optional `window/workDoneProgress/create` round trip. Also tidy a small `Pkg.Apps.add` invocation in the testrunner documentation. This change requires upgrading the `testrunner` CLI to a version that understands `--read-stdin`; CHANGELOG covers the install and verification steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ce files
For unsaved (`untitled:`/`buffer:`) URIs the `filepath` we send to
`testrunner` is a virtual identifier with no `dirname`, so without an
explicit base the subprocess resolves relative `include` calls in the
buffer relative to its own cwd. Pass `--root-path=<workspace_root>`
in this case, sourced from `state.root_path` (the workspace folder
from `initialize`), so `include("helpers.jl")` from an unsaved buffer
finds the workspace's `helpers.jl`.
`state.root_path` is the right value here, not `state.root_env_path`:
the env path is the directory containing `Project.toml`, which lives
on a different axis from "where workspace-relative includes resolve".
Saved files don't need the flag at all because their `filepath` has
a real `dirname`, so `testrunner_cmd` omits `--root-path` for them.
Requires the `testrunner` CLI to be built from a TestRunner.jl
revision that understands `--root-path` (see CHANGELOG for the
install/upgrade command).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* testrunner: Run tests against the live editor buffer
Pipe the current editor buffer to the `testrunner` subprocess via
stdin (using the new `--read-stdin` flag) instead of relying on the
on-disk file. Drop the `get_saved_file_info` / sourcetext sync check
in `testrunner_run_testset_from_uri` and
`testrunner_run_testcase_from_uri`, so unsaved edits — including
brand-new `@testset` / `@test` cases — execute as-is from code lens
and code action.
Plumb the buffer source through `_testrunner_run_testset` /
`_testrunner_run_testcase` (extracting it from
`fi.parsed_stream` for testset, and capturing it on the
`TestRunnerTestcaseProgressCaller` for testcase) so it survives the
optional `window/workDoneProgress/create` round trip.
Also tidy a small `Pkg.Apps.add` invocation in the testrunner
documentation.
This change requires upgrading the `testrunner` CLI to a version that
understands `--read-stdin`; CHANGELOG covers the install and
verification steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* testrunner: Pass `--root-path` so unsaved buffers can include workspace files
For unsaved (`untitled:`/`buffer:`) URIs the `filepath` we send to
`testrunner` is a virtual identifier with no `dirname`, so without an
explicit base the subprocess resolves relative `include` calls in the
buffer relative to its own cwd. Pass `--root-path=<workspace_root>`
in this case, sourced from `state.root_path` (the workspace folder
from `initialize`), so `include("helpers.jl")` from an unsaved buffer
finds the workspace's `helpers.jl`.
`state.root_path` is the right value here, not `state.root_env_path`:
the env path is the directory containing `Project.toml`, which lives
on a different axis from "where workspace-relative includes resolve".
Saved files don't need the flag at all because their `filepath` has
a real `dirname`, so `testrunner_cmd` omits `--root-path` for them.
Requires the `testrunner` CLI to be built from a TestRunner.jl
revision that understands `--root-path` (see CHANGELOG for the
install/upgrade command).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an `active_finally_labels` stack to `EventLinearizer`, pushed
before linearizing a `K"tryfinally"` try body and popped before its
finally body. When `K"return"` is processed inside the try, it now
routes through the innermost active finally label instead of switching
directly to an unreachable phantom block. This matches the runtime
semantics — `finally` runs before the return takes effect — so an
assignment preceding the return is live with respect to a use in the
finally body.
Previously, `lowering/unused-assignment` flagged such assignments as
dead stores, since CFG paths from the assignment block could not
reach the finally use without going through the "other assignments"
avoid set.
```julia
function f()
local x::Float64
try
y = sin(rand((rand(), Inf)))
x = y # Value assigned to `x` is never used
return 0
catch
x = 0.0 # Value assigned to `x` is never used
return 1
finally
push!(xs, x)
end
end
```
`K"tryfinally"`'s `try_body_terminated` check is unaffected:
`is_block_reachable_from_entry` ignores pending gotos to as-yet
-unemitted labels, so the new pending `(_, finally_label)` edges from
returns are not seen during the check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inside a `K"tryfinally"` try body, the `K"block"` handler now emits
a `gotoifnot` to the innermost active finally label after each
statement. Combined with the existing entry-bypass `gotoifnot` at
try entry, this covers exception escape at every statement boundary
in the try body.
Previously the entry-bypass alone modeled "exception immediately
upon entering try", missing the case where earlier statements had
already executed before the throw. As a result `lowering/unused-
assignment` flagged intermediate assignments to the same variable
in idioms like:
state = "step1 starting"
step1() # might throw — state="step1 starting"
# would be the live value in finally
state = "step2 starting"
step2()
state = "done"
as dead stores, since dead-store analysis avoids paths through
other assignments and the only way to reach the finally use was
through `state = "done"`. Now each intermediate assignment has a
direct edge to the finally body via the inserted `gotoifnot`, so
its value is correctly considered live.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cfg-analysis: Route `return` inside `try` through enclosing `finally`
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.
This PR releases version
2026-05-06.Checklist
release / Test JETLS.jl with release environmentrelease / test_app / Test jetls serverelease / test_app / Test jetls checkrelease / test_app / Test jetls schemarelease / check_schemas / Check schemas are up to datePost-merge
releases/2026-05-06branch can be deleted after merging