Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.13 and JDK 11 - IllegalAccessError: Update to non-static final field #12881

Closed
pvlugter opened this issue Sep 25, 2023 · 1 comment · Fixed by scala/scala#10556
Closed

2.13 and JDK 11 - IllegalAccessError: Update to non-static final field #12881

pvlugter opened this issue Sep 25, 2023 · 1 comment · Fixed by scala/scala#10556
Assignees
Milestone

Comments

@pvlugter
Copy link

pvlugter commented Sep 25, 2023

Uncovered by akka/akka#32128 when dropping JDK 8 in Akka (akka/akka#32127).

Additional case for scala/scala-dev#408 and #12340.

Trait fields are still marked final when implemented by an anonymous class in a lazy val.

Reproduction steps

scalaVersion := "2.13.12"
scalacOptions ++= Seq("-release", "11")
trait T {
  val foo = "foo"
}

object Test {
  def main(args: Array[String]): Unit = {
    lazy val t = new T {}
    println(t)
  }
}

Problem

java.lang.IllegalAccessError: Update to non-static final field Test$$anon$1.foo attempted from a different method (T$_setter_$foo_$eq) than the initializer method <init>

Field marked final:

:javap -p -v Test$$anon$1

...

  private final java.lang.String foo;
    descriptor: Ljava/lang/String;
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

When it's either not a lazy val or not an anonymous class, the field is non-final:

  private java.lang.String foo;
    descriptor: Ljava/lang/String;
    flags: (0x0002) ACC_PRIVATE
@lrytz lrytz self-assigned this Sep 25, 2023
@lrytz lrytz added this to the 2.13.13 milestone Sep 25, 2023
@lrytz
Copy link
Member

lrytz commented Sep 26, 2023

This was a hard one to pin down...

Example

trait T { val foo = "foo" }
class C {
  def m = {
    lazy val t = new T {}
    /* expands to
    lazy val t = {
      class anon extends T {
        private var _foo
        def foo = _foo
        def T$_setter_$foo_= (s: String) = { _foo = s}
      }
      new anon
    }
    */
    t
  }
}

Fields info transformer:

  • adds field symbol to class for field inherited from a trait
  • the field symbol does not have the MTUABLE flag

Fields tree transformer:

  • calls afterOwnPhase(clazz.info.decls) on the anonymous class, this forces the fields info transform
    • the type history has a new entry tailcalls -> ClassInfoType(...) (tailcalls is after fields)
  • tree transformer adds the MUTABLE flag to the field symbol when generating the setter
  • tree transformer creates the t$lzycompute accessor and calls typer on it
    • accessor body: t$lzy.synchronized { if (t$lzy.initialized) t$lzy.value else { t$lzy.initialize({ <anon> }}
    • typedApply of synchronized(...) -> inferMethodInstance -> TreeTypeSubstituter for type parameter of synchronized
    • TypeMapTreeSubstituter (parent of TreeTypeSubstituter) calls if (tree.isDef) tree.symbol modifyInfo typeMap
    • modifyInfo on the anonymous class symbol is destructive for the type history:
      => setInfo(f(info)) takes the current info and creates a new TypeHistory with just one element
      => the type history has the entry fields -> ClassInfoType(...) (the type itself is unchanged)

When obtaining the anonymous class info in a later phase, the fields info transformer will run again because the current type is only valid until fields. A new foo field symbol is created, it doesn't have the MUTABLE flag, so it ends up final in bytecode.

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 a pull request may close this issue.

2 participants