Skip to content

release: 2026-05-08#684

Merged
aviatesk merged 17 commits into
releasefrom
releases/2026-05-08
May 9, 2026
Merged

release: 2026-05-08#684
aviatesk merged 17 commits into
releasefrom
releases/2026-05-08

Conversation

@aviatesk

@aviatesk aviatesk commented May 9, 2026

Copy link
Copy Markdown
Owner

This PR releases version 2026-05-08.

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-08 branch can be deleted after merging
  • CHANGELOG.md will be automatically updated on master

github-actions Bot and others added 17 commits May 6, 2026 12:52
`rewrite_closure_block`'s `any_changed=false` fallback called
`JS.mapchildren` over the same children the loop above had already
recursed into via `_rewrite_local_closures_to_opaque`, doubling the
work at every block. Across nested blocks (each `function` / `for` /
`if` / `try` body) the cost compounded to `O(2^depth)`, so a real-
world file like `CSV/src/file.jl` (957 lines, ~10–20 levels of nested
blocks) never finished — 6+ minutes and 3+ GB RSS before being
killed.

`new_children` already holds the recursively-rewritten children
regardless of whether sibling collapse fired, so dropping the
fallback (and the now-unused `any_changed` flag) is sufficient. After
the fix the same file completes in roughly 0.5–0.8 s post-warmup
across repeated runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old-style `Base.@assume_effects` macro expansion collapses source
positions to line granularity, which breaks LSP features (hover,
diagnostics, find-references, ...) for identifiers inside the
annotated body.

This stub re-implements `@assume_effects` as a JuliaLowering new-style
macro so provenance is preserved through expansion. Since the
`Expr(:purity)` / `Expr(:meta)` directives the real macro emits are
irrelevant to LSP analysis, the stub:

- validates the effect setting names against the list in
  `Base.compute_assumed_setting` (`base/expr.jl`), with `!` negation
  and `!!` chains supported,
- routes the body (function definition, `@ccall`, or call-site
  annotation) through unchanged, and
- expands the no-body declaration form to a placeholder.

`:consistent_overlay` and `:nortcall` are deliberately rejected as
standalone inputs since Base only sets them through the `:foldable`
and `:total` shortcuts. Unrecognized setting names produce a
`MacroExpansionError` pointing at the offending argument so JETLS
surfaces them as `lowering/macro-expansion-error` diagnostics.

`@assume_effects` is also added to `NEW_STYLE_MACROCALL_NAMES` so
`_remove_macrocalls` doesn't fall back to the old-style rewrite path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add new-style JuliaLowering implementations of `Base.@inline`,
`Base.@noinline`, `Base.@propagate_inbounds`, and `Base.@inbounds` in
`src/utils/jl-syntax-macros.jl`. These decoration macros previously
went through legacy expansion that injected `Expr(:meta, …)` markers,
producing synthetic nodes with no source byte range. The new
implementations drop the markers and return the wrapped expression
unchanged, preserving the inner funcdef's provenance through the
lowering pipeline.

TypeAnnotation.jl now registers *every* provenance entry, not just the
first provenance. For macro-wrapped surface forms like
`@inline f(x) = body` whose chain is `[macrocall, function]`,
it allows the inner funcdef's span to be queryable in addition to the
macrocall's outer span.
Add a regression test for 3a648d0. `rewrite_closure_block`'s old
`any_changed=false` fallback re-recursed via `JS.mapchildren` over
children the loop above had already rewritten, doubling the work at
every block. Across `D` nested blocks the cost compounded to
`O(2^D)`, so real-world files like `CSV/src/file.jl` (~10–20 levels
of nested blocks) took unreasonable time to be rewritten.
Extract the repeated "look up a binding by kind/name and verify
its last-provenance entry's sourcetext" pattern in
`test/utils/test_jl_syntax_macros.jl` into a shared
`assert_binding_provenance` helper, and rewrite the existing
`@spawn`/`@something`/`@test`/`@testset`/`@assume_effects`
"binding resolution preserves provenance" testsets to use it.

The helper returns `(binfo, provs)` so callers that need to
assert further on the provenance entry (e.g. `@testset`'s scope
isolation test, which also checks `JS.source_location`) can
chain those assertions without re-running the lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add new-style JuliaLowering stubs for `Test.@test_throws`,
`Test.@test_broken`, `Test.@test_skip`, `Test.@test_warn`,
`Test.@test_nowarn`, `Test.@test_logs`, `Test.@test_deprecated`,
and `Test.@inferred`. The real macros wrap user-written bodies
in test-recording / exception-catching scaffolding that destroys
provenance during expansion; the stubs drop the scaffolding and
emit the user-written sub-expressions in a `block` (or alone,
for single-body forms) so identifiers inside every argument flow
through scope resolution with their byte ranges intact.

For macros with the `body kws...` shape (`@test_broken`,
`@test_skip`) we reuse `_validate_test_kw` and drop the kws
after shape validation. For `@test_logs`, where kws sit
alongside a list of patterns and a body, we keep only the kw
RHS — that's where any user-written identifier needing scope
resolution lives.

Register the new names in `NEW_STYLE_MACROCALL_NAMES`, merge the
previously-split `@test`/`@testset` doc comments into a single
header covering all `Test.jl` stubs, and move the `@testset`
rationale about `let`-block scope isolation inline next to the
let-block construction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es (#676)

When a comprehension / `map` / `filter` auto-generated lambda's source
bytes coincide with the user's yield expression, the lowered tree puts
`K"new_opaque_closure"`, `K"opaque_closure_method"`, the argt/rt_lb
svec helpers, and the OC binding `K"="` / `K"slot"` all at that same
byte range. `tmerge_at_range` would otherwise surface
`Union{T, Method, OpaqueClosure, Type}` instead of the body's actual
value type. Note that comprehension with untyped iterator masked this
currently — `Any` from `most_general_argtypes` absorbed everything —
but typed forms (`for x::T in xs`) make it visible.

Filter structurally rather than filtering by matching against inferred
lattice element: track which `K"opaque_closure_method"`'s body each
lowered node belongs to (the new `oc_body_scope` index in
`InferredTreeContext`) and at query time keep only the nodes whose body
matches the queried range. Inner-OC construction noise sitting inside
an outer OC body — multi-`for` comprehension, closure-of-closure — is
filtered when querying at the inner OC's byte range, while a reference
to an explicit closure binding (no OC construction at the byte range)
still surfaces its `OpaqueClosure{…}` type unchanged.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`find_method_and_sig_call` matched `sig_call` by "first svec_call DFS
finds", which mis-attributed the inner OC's user-argtypes to the outer
OC when nested closures embedded the inner's `method_defs` inside the
outer's lambda body (cartesian comprehensions, manually nested typed
closures, etc.). The outer OC was then built with the inner's argt
(`Tuple{Float64}` instead of `Tuple{Int}`), surfacing as a `TypeError`
at the OC call site at runtime, and as a wrongly-typed captured
parameter (`x::Float64` for `for x::Int in ...`) in inlay hints.

Split the lookup into `find_method_node` / `find_sig_call_for` and
match the sig_call by `var_id` against `method_node[2]`.
`compute_diagnostic_result_id` previously keyed the resultId only on
the file version (and dependency versions for files with explicit
imports). When `[diagnostic]` config changed,
`handle_lsp_config_change!`
sent `workspace/diagnostic/refresh`, but the client's re-request with
the previous `previousResultId` matched the freshly-computed resultId
and the server returned `Unchanged`, so the refresh was effectively a
no-op and stale diagnostics persisted on the client.

Fold the current `DiagnosticConfig` value into the resultId so any
`[diagnostic]` config mutation flips the resultId and the refresh
takes effect.

Also defines `Base.hash(::DiagnosticConfig, ::UInt)` content-based.
`@option` from Configurations.jl generates `==` but not `hash`, so the
default `hash` fell back to `objectid` and broke the `==`/`hash`
contract — equal-valued `DiagnosticConfig` instances hashed
differently because of the embedded `patterns::Vector`. Without this,
the resultId fold above would spuriously invalidate the client cache
whenever `ConfigManagerData` was rebuilt with the same values.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `textDocument/diagnostic message cycle` test only checked
`resultId` behavior, so plumbing regressions that silently dropped
diagnostic content would go unnoticed. The previous fixture
(`x = 1` → `x = 2`) also produced no diagnostics in any state,
meaning the `allow_unused_underscore` config flip exercised the
`resultId` path but had no effect on actual content.

Switch the fixture to `func(x) = nothing` → `func(_x) = nothing`,
so each transition meaningfully changes both `resultId` and
diagnostic content:

- initial: 1 unused-argument diagnostic on `x`
- after edit to `_x`: 0 diagnostics (default
  `allow_unused_underscore=true` suppresses `_`-prefixed names)
- after config flip to `allow_unused_underscore=false`: 1
  unused-argument diagnostic on `_x`

Each full-report assertion now also checks the diagnostic count
and `code`, so a regression that breaks pull-mode diagnostic
delivery or config propagation would surface here.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the `@option` macro from Configurations.jl with `@kwdef` plus
`@define_eq_overloads` for the eleven config structs in `src/types.jl`,
and hand-roll the dict-to-struct hydration that the package needs.

`parse_config_from_dict` (was `Configurations.from_dict`) recursively
populates a `ConfigSection` from a config dict, threading a dotted
key path through nested calls. `InvalidKeyError` now carries that
full path directly, which lets `parse_config_dict` report the
offending key without re-walking the user's dict against a canonical
default — eliminating `collect_unmatched_keys`,
`UntypedConfigDict`, `DEFAULT_UNTYPED_CONFIG_DICT`, and the
`Configurations.to_dict`-based scaffolding.

`parse_config_dict_value` handles the small type tree we actually
use: `Maybe{T}`, nested `ConfigSection`, vectors, the formatter
union, and plain `convert`-able leaves. Errors at every level are
formatted via `parse_dict_error` so users get
`Invalid value at \`a.b.c\`: expected X, got Y` regardless of where
in the type tree the mismatch occurred. The bespoke
`DiagnosticPattern` / `AnalysisOverride` validators are inlined in
`parse_config_dict_value` rather than dispatched through a hook,
matching how `Maybe{FormatterConfig}` is handled — the set of
special cases is small and closed.

`Maybe{T} = Union{Nothing,T}` is now defined locally. Tests that
poked into `Configurations.from_dict` / `Configurations.InvalidKeyError`
are updated to use the JETLS-side equivalents, and new tests cover
`InvalidKeyError.path` and `parse_config_dict_value`'s path-aware
error messages.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `hash` method generated by `@define_eq_overloads` started with
`h = $h_init`, which discarded the input `h::UInt` argument and
broke the standard `hash(x, h)` contract. Calls like
`Base.hash(x.$fld, h)` for fields wrapped in such types would lose
the running hash state.

Drop the per-type random salt entirely — type dispatch and field
values already disambiguate — and chain `Base.hash` calls through
the input `h`.

Also drop a redundant `::DiagnosticConfig` annotation at the
`compute_diagnostic_result_id` callsite.
Add `remove_stale_vendored_packages` step to `vendor-deps.jl` so
that directories under `vendor/` not present in the latest set
collected by `collect_packages_to_vendor()` are deleted before
copying the new sources. This keeps `vendor/` in sync when a
dependency is dropped from the project.

Non-directory entries under `vendor/` are left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aviatesk
aviatesk merged commit 72cc49c into release May 9, 2026
10 checks passed
@aviatesk
aviatesk deleted the releases/2026-05-08 branch May 9, 2026 03:22
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