Skip to content

Contract eta-expanded type constructors - #147

Merged
rochala merged 1 commit into
mainfrom
fix/eta-expanded-lambdas
Aug 24, 2026
Merged

Contract eta-expanded type constructors#147
rochala merged 1 commit into
mainfrom
fix/eta-expanded-lambdas

Conversation

@rochala

@rochala rochala commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

First of the output-alignment fixes from the Scala 2 / Scala 3 stdlib diff. Independent of #146.

The bug

TASTy stores a type constructor passed where F[_] is expected as the eta-expansion [A] =>> C[A], and the printer rendered it literally. So the Scala 3 output was worse than the Scala 2 output for the very same class — and inconsistent within a single line, with List plain and the other constructor expanded:

// Scala 3 (TASTy), before:
class NonEmptyList[+A] extends NonEmptyCollection[A, List, [A] =>> NonEmptyList[A]] with Product with Serializable
// Scala 2 (pickles), same class:
class NonEmptyList[+A] extends NonEmptyCollection[A, List, NonEmptyList] with Product with Serializable
// Scala 3, after — now identical to the Scala 2 rendering:
class NonEmptyList[+A] extends NonEmptyCollection[A, List, NonEmptyList] with Product with Serializable

This was 51 of the 132 differing lines in the stdlib comparison — the largest category, and every one favoured the Scala 2 rendering.

Scope of the contraction

Only a genuine eta-expansion: every parameter passed straight through, in order, with no bounds of its own. Two things deliberately keep their current rendering, and two pre-existing tests pin them:

  • [A <: AnyRef] =>> F[A] — the bound says something F does not.
  • [A] =>> F[G[A]] — a composition, not a plain constructor.

Verified against a real artifact: cats.Applicative still shows all 11 of its [α] =>> F[G[α]] composition lambdas.

Verification

  • End-to-end per the CLAUDE.md rule, on cats-core_3:2.10.0: NonEmptyList now renders identically to its Scala 2 counterpart, =>> count 0; Applicative unchanged at 11.
  • lib.test 372/372.
  • The new fixture (CellarSelfBox) reproduces the case, and the test was confirmed to fail without the fix: eta-expansion not contracted: class CellarSelfBox[A] extends CellarBox[[A] =>> CellarSelfBox[A]].

Remaining alignment items (verified, not in this PR)

  • scala.jdk.CollectionConverters shows 20 class *HasAs* wrappers but 0 of the defs that attach them — PublicApiFilter.isSyntheticSym drops them.
  • scala.Char emits val MaxValue: with a raw U+FFFF and val MinValue: with a raw NUL into the Markdown.

Two categories from the diff turned out not to be cellar bugs: the $default$N accessors don't reproduce outside the experimental stdlib build (0 occurrences on cats-core_3, cats-effect_3, pureconfig), and toString() vs toString reflects genuinely different declarations in the two encodings.

🤖 Generated with Claude Code

@rochala
rochala force-pushed the fix/eta-expanded-lambdas branch from 50b10c9 to fbf63d7 Compare August 23, 2026 14:47
Passing `C` where `F[_]` is expected makes the compiler store the
eta-expansion `[X] =>> C[X]` in TASTy, and the printer rendered it
literally. That made the Scala 3 output worse than the Scala 2 output for
the very same class, and inconsistent within a single line — `List` plain,
the other constructor expanded:

  class NonEmptyList[+A] extends NonEmptyCollection[A, List, [A] =>> NonEmptyList[A]] ...
  class NonEmptyList[+A] extends NonEmptyCollection[A, List, NonEmptyList] ...        // now

Found by diffing the 2.13 stdlib read from pickles against the same
library recompiled to TASTy: 51 of the 132 differing lines were this, the
largest category, and every one favoured the Scala 2 rendering.

Contracted only in argument position, and only for a lambda that passes
its parameters straight through, in order, with no bounds of their own.

The position restriction matters. A hand-written `type F = [A] =>> C[A]`
is a different declaration from `type F = C` — its parameter is invariant
where C's may not be — and nothing here can tell the two apart: they are
structurally identical, and the parameter variance that separates them is
package-private in tasty-query. Contracting everywhere would print two
distinct sources identically. In argument position the expansion is the
compiler's doing; as the right-hand side of an alias it is the author's,
so that is left alone. Both directions are pinned by tests.

`[A <: AnyRef] =>> F[A]` and `[A] =>> F[G[A]]` keep their rendering, as
two pre-existing tests require. Verified against cats-core_3:
cats.Applicative still shows its 11 composition lambdas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rochala
rochala force-pushed the fix/eta-expanded-lambdas branch from fbf63d7 to 75a18a0 Compare August 23, 2026 15:40
@rochala
rochala merged commit 6d357af into main Aug 24, 2026
6 checks passed
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