typer: remove by-name lifting and replace default-getter args with fresh syms#25502
Merged
odersky merged 1 commit intoscala:mainfrom Mar 17, 2026
Merged
typer: remove by-name lifting and replace default-getter args with fresh syms#25502odersky merged 1 commit intoscala:mainfrom
odersky merged 1 commit intoscala:mainfrom
Conversation
…esh syms fix scala#24201 This change also fixes scala#18123 (not fully understand why) **Background** `LiftToDefs` lifts by-name arguments to synthetic `def`s so that a method argument would not be duplicated both as the real argument and as an argument to a default getter. see scala#2939 A transformation for by-name semantics is done later in `ElimByName`, which turns a by-name actual argument into a closure. **Problem** However, lifting by-name arguments can be problematic in a specific scenario. scala#24201 For example, in `extends Foo[Baz](Baz.E1, arg2 = 3)`: ```scala abstract class Foo[T](value: => T, arg1: Int = 1, arg2: Int = 2) enum Baz { case E1 } object Baz extends Foo[Baz](Baz.E1, arg2 = 3) ``` `LiftToDefs` lifts the by-name argument `Baz.E1` to the instance method like: ```scala object Baz extends { def defaultValue$1: Baz = Baz.E1 // instance method val arg1$1: Int = Foo.$lessinit$greater$default$2[Baz] new Foo[Baz](defaultValue$1, arg1$1, arg2 = 3) } ``` where `defaultValue$1` is a synthesized instance method owned by the constructor. This is not valid code, because the constructor super call needs to access uninitialized `this` to call `defaultValue$1`, which results in a `VerifyError` during JVM bytecode verification. **Why previous patches did not work** - scala#24983 tried to undo lifting in a later phase (`HoistSuperArgs`) - scala#25157 skips lifting when the owner is a constructor but neither worked well, because both essentially cancel lifting in the specific code shape (when inside a constructor super call). However, cancelling lifting reintroduces the problem fixed by scala#3839 for those code shapes. **Solution** This commit fixes scala#24201 without breaking scala#2939 by removing `LiftToDefs` entirely. Instead, we avoid the duplicated symbol problem by copying by-name parameter arguments with fresh symbols. This removes the constructor-local synthetic-method path entirely and keeps the code as-is. (This is not a problem regarding evaluation order, because `ElimByName` translates `Baz.E1` into the closure `() => Baz.E1`.) ```scala object Baz extends Foo[Baz](Baz.E1, arg2 = 3) ``` The default-getter handling is moved to the point where duplication actually occurs (`Applications.spliceMeth`). When `spliceMeth` rebuilds a default-getter call, it now freshens only by-name arguments. For example, the example code below: ```scala // i7477.scala def spawn(f: => Unit)(naming: Int = 4): Unit = ??? def test(): Unit = spawn { val x: Int = 5 x }() ``` previously, it is transformed into: ```scala def f$1: Unit = { val x: Int = 5 { x; () } } this.spawn(f$1)(this.spawn$default$2(f$1)) ``` now, the same tree is transformed into: ```scala this.spawn({ val x$1: Int = 5 { x$1 () }} )(this.spawn$default$2({ val x$2: Int = 5 { x$2 () } }) ) ```
odersky
approved these changes
Mar 17, 2026
Contributor
odersky
left a comment
There was a problem hiding this comment.
Looks like the right fix. It's true that this might copy a lot of code if default arguments are large, but I don't think this is a common case.
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.
fix #24201
This change also fixes #18123 (not fully understand why)
Background
LiftToDefslifts by-name arguments to syntheticdefs so that a method argument would not be duplicated both as the real argument and as an argument to a default getter. see #2939A transformation for by-name semantics is done later in
ElimByName, which turns a by-name actual argument into a closure.Problem
However, lifting by-name arguments can be problematic in a specific scenario. #24201
For example, in
extends Foo[Baz](Baz.E1, arg2 = 3):LiftToDefslifts the by-name argumentBaz.E1to the instance method like:where
defaultValue$1is a synthesized instance method owned by the constructor. This is not valid code, because the constructor super call needs to access uninitializedthisto calldefaultValue$1, which results in aVerifyErrorduring JVM bytecode verification.Why previous patches did not work
HoistSuperArgs)but neither worked well, because both essentially cancel lifting in the specific code shape (when inside a constructor super call). However, cancelling lifting reintroduces the problem fixed by #3839 for those code shapes.
Solution
This commit fixes #24201 without breaking #2939 by removing
LiftToDefsentirely. Instead, we avoid the duplicated symbol problem by copying by-name parameter arguments with fresh symbols.This removes the constructor-local synthetic-method path entirely and keeps the code as-is. (This is not a problem regarding evaluation order, because
ElimByNametranslatesBaz.E1into the closure() => Baz.E1.)The default-getter handling is moved to the point where duplication actually occurs (
Applications.spliceMeth). WhenspliceMethrebuilds a default-getter call, it now freshens only by-name arguments.For example, the example code below:
previously, it is transformed into:
now, the same tree is transformed into:
How much have your relied on LLM-based tools in this contribution?
work with GPT5.4 under supervision
How was the solution tested?
Additional notes
Maybe this could be a code size problem when the by-name parameter arg has a huge body?