Skip to content

release: 2026-05-06#675

Merged
aviatesk merged 27 commits into
releasefrom
releases/2026-05-06
May 6, 2026
Merged

release: 2026-05-06#675
aviatesk merged 27 commits into
releasefrom
releases/2026-05-06

Conversation

@aviatesk

@aviatesk aviatesk commented May 6, 2026

Copy link
Copy Markdown
Owner

This PR releases version 2026-05-06.

Checklist

  • release / Test JETLS.jl with release environment
  • release / test_app / Test jetls serve
  • release / test_app / Test jetls check
  • release / test_app / Test jetls schema
  • release / check_schemas / Check schemas are up to date

Post-merge

  • The releases/2026-05-06 branch can be deleted after merging
  • CHANGELOG.md will be automatically updated on master

github-actions Bot and others added 27 commits May 5, 2026 02:25
This needs a more general fix.
TypeAnnotation: Resolve local closure types via `OpaqueClosure`
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`
@aviatesk
aviatesk merged commit 732c537 into release May 6, 2026
10 checks passed
@aviatesk
aviatesk deleted the releases/2026-05-06 branch May 6, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant