Skip to content

typer: remove by-name lifting and replace default-getter args with fresh syms#25502

Merged
odersky merged 1 commit intoscala:mainfrom
tanishiking:i24201-2
Mar 17, 2026
Merged

typer: remove by-name lifting and replace default-getter args with fresh syms#25502
odersky merged 1 commit intoscala:mainfrom
tanishiking:i24201-2

Conversation

@tanishiking
Copy link
Member

@tanishiking tanishiking commented Mar 12, 2026

fix #24201
This change also fixes #18123 (not fully understand why)

Background
LiftToDefs lifts by-name arguments to synthetic defs so that a method argument would not be duplicated both as the real argument and as an argument to a default getter. see #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. #24201

For example, in extends Foo[Baz](Baz.E1, arg2 = 3):

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:

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

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 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.)

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:

// i7477.scala
def spawn(f: => Unit)(naming: Int = 4): Unit = ???
def test(): Unit = spawn {
  val x: Int = 5
  x
}()

previously, it is transformed into:

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:

this.spawn({
  val x$1: Int = 5
  {
    x$1
    ()
  }}
)(this.spawn$default$2({
  val x$2: Int = 5
  {
    x$2
    ()
  }
  })
)

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?

…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
    ()
  }
  })
)
```
Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@odersky odersky merged commit 34e8ec6 into scala:main Mar 17, 2026
64 checks passed
@tanishiking tanishiking deleted the i24201-2 branch March 18, 2026 01:27
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.

Missing argument results in java.lang.VerifyError Regression in implicit resoultion for arguments passed by name

2 participants