Preserve user-defined closure parameter names under CC#25999
Open
bracevac wants to merge 1 commit into
Open
Conversation
Fixes scala#25971 by recording user-written closure parameter names against their inferred plain `FunctionN` type at typer time, propagating that registration when `AppliedType.derivedAppliedType` rebuilds the type (PostTyper's `@caps.declared` insertion, capture-set substitution, etc.), and reading it back in `cc.Setup.normalizeFunctions` when the type is expanded into a dependent function. This replaces the synthetic `x$N` names that were previously generated by that expansion. The mechanism is encapsulated in `cc.ClosureParamNames`, which exposes three operations (`record`, `propagate`, `of`) over a per-Run `EqHashMap` table. Off the CC code path the table stays empty, so the hot-path call from `core.Types.AppliedType.derivedAppliedType` is a no-op gated on `Feature.ccEnabledSomewhere`.
bracevac
added a commit
to dotty-staging/dotty
that referenced
this pull request
May 7, 2026
Tries the alternative to scala#25999: when capture checking is enabled, force `Closure.toFunctionType(alwaysDependent = true)`, so the inferred closure type is `RefinedType(FunctionN, apply, MethodType)` from the start. The user-given parameter names live structurally in the MethodType refinement and survive every type rebuild trivially. Two iterations of effort: 1. Bare `alwaysDependent = true`: 201 captures tests fail. Setup's `apply` short-circuits for `RefinedFunctionOf` and never wraps with a capture-set variable, so the closure's body captures have nowhere to be written; subsequent CC checks see the un-captured frozen type and reject. 2. Wrap eager dep-fun in `@caps.internal.inferred` so Setup's `mapInferred` matches the AnnotatedType branch, drops the marker, and routes through `addVar` (which gives the expected `CapturingType(RefinedType, {var})`): down to 49 failures. After `--update-checkfiles`, 23 still fail. The remaining 23 are not format changes — they're semantic regressions where the eager dep-fun shape causes type inference to take a different path. Example: `tests/neg-custom-args/captures/vars.scala` line 39 (the bare reference `g`) now errors when it shouldn't, because the dep-fun shape leaks `g`'s body captures into a context that previously left them abstract. Conclusion: making this approach correct would require rethinking how CC's pipeline carries captures from typer into Setup — well beyond the scope of recovering closure parameter names. The side-table approach in scala#25999 sidesteps all of this by leaving structural types alone and only annotating them for naming. Includes: - TypeAssigner.assignType(Closure): force `alwaysDependent` under CC, guard with `\!ctx.erasedTypes`, wrap result in `@InferredAnnot`.
This was referenced May 7, 2026
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.
Fixes #25971.
Records user-written closure parameter names against their inferred plain
FunctionNAppliedTypeat typer time, propagates that registration through type rebuilds (PostTyper's@caps.declaredinsertion, capture-set substitution), and reads it back incc.Setup.normalizeFunctionswhen the type is expanded into a dependent function. This replaces the syntheticx$Nnames that the expansion otherwise generates.Design
The mechanism is encapsulated in
cc.ClosureParamNames— three operations over a per-RunEqHashMap:TypeAssigner.assignType(Closure, ...)record(funType, methodicType)AppliedTyperebuildsTypes.AppliedType.derivedAppliedTypepropagate(this, newTp)FunctionN→ dep functioncc.Setup.normalizeFunctionsof(original)asparamNamesOff the CC code path the table stays empty, and
propagateshort-circuits onFeature.ccEnabledSomewhere, so the hot-path call fromcore.Typesis essentially free for non-CC compilation.Why a side table
An earlier walker-based attempt (#25982) recovered names by aligning the val/result type spine with the rhs closure tree. It works but is invasive (~140 lines of tree-type alignment, with
peelFormal,collectArgsThroughFormal, etc.) and doesn't extend cleanly to all the type shapes CC produces. The side table records once per closure and follows the type wherever it goes.A typer-level alternative — always emitting
RefinedType(FunctionN, apply, MT)for closures — would make every closure structurally a dependent function, which we don't want. The annotation alternative would coupleFunctionOrMethodextractors to a CC-specific annotation, with similarly broad fallout. The side table localizes the CC concern:cc/ClosureParamNames.scalaowns the policy, and call sites are one-liners.Test impact
Previously-synthetic
x$Nnames in error messages now resolve to user-given names where applicable. Updated check files:tests/neg-custom-args/captures/heal-tparam-cs.check—c1instead ofx$0tests/neg-custom-args/captures/i15923.check,i15923b.check—cap,lcaptests/neg-custom-args/captures/scope-extrusions.check—xinstead ofx$0tests/neg-custom-args/captures/sep-curried-par.check—p1instead ofx$0tests/neg-custom-args/captures/use-capset.check—xsinstead of unnamedNew printing test
tests/printing/cc/i25971.scalacovers three closure shapes:[C^] => (xs) => (ys) => (zs) => …)(ys) => (zs) => …)new Box(closure)) — the substitution case that breaks naive identity-based recovery.Full
CompilationTestssuite passes (73/73).How much have you relied on LLM-based tools in this contribution?
Extensively, while I specified how I expect the solution to look like/work.
How was the solution tested?
New automated tests (
tests/printing/cc/i25971.scala, plus updated check files for affected neg tests).