Binary compatibility breakage when adding `super` call in `trait`

If I change this

trait Parent {
  def foo() = 1
}
trait Child extends Parent {
  override def foo() = 2
}

to this

trait Parent {
  def foo() = 1
}
trait Child extends Parent {
  override def foo() = super.foo() + 1
}

MIMA’s binary compat checker fails with

Found 1 issue when checking against com.lihaoyi:foo1:VersionConstraint.Lazy(0.0.1, VersionInterval(None, None, false, false), Some(Version(0.0.1)), None)
 * abstract synthetic method Child$$super$foo()Int in interface Child is present only in current version
   filter with: ProblemFilter.exclude[ReversedMissingMethodProblem]("Child.Child$$super$foo")

This causes considerable inconvenience in Mill, where changing the implementation of something to call super is not uncommon. It can be worked around by doing something like

trait Parent {
  def fooNewHelper() = 1
  def foo() = fooNewHelper()
}
trait Child extends Parent {
  override def foo() = fooNewHelper() + 1
}

But doing these workarounds is super annoying

Two questions:

  1. Is this a real error that I need to be worried about? Like if I publish one of these changes, will my users start seeing MethodNotFound errors? Or is it an incidental change that doesn’t actually break things?
  2. Can it be fixed somehow so changing the implementation of a method does not randomly cause a new abstract method to be generated, and break binary compatibility?

For a repro, create a Mill project with a ./mill script and:

build.mill

//| mill-version: 1.0.0
//| mvnDeps: [com.github.lolgab::mill-mima_mill1:0.2.0]
import mill._, scalalib._, publish._, com.github.lolgab.mill.mima.Mima
object foo1 extends ScalaModule with PublishModule{
  def scalaVersion = "3.7.1"
  def publishVersion = "0.0.1"
  def pomSettings = PomSettings(
    description = "Main method argument parser for Scala",
    organization = "com.lihaoyi",
    url = "https://github.com/com-lihaoyi/unroll",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github("com-lihaoyi", "unroll"),
    developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
  )
}
object foo2 extends ScalaModule with Mima{
  def scalaVersion = "3.7.1"
  def mimaPreviousArtifacts = Task { Seq(mvn"com.lihaoyi::foo1:0.0.1") }
}

foo1/src/MyTrait.scala

trait Parent {
  def foo() = 1
}
trait Child extends Parent {
  override def foo() = 2
}

foo2/src/MyTrait.scala

trait Parent {
  def foo() = 1
}
trait Child extends Parent {
  override def foo() = super.foo() + 1
}

And run

./mill foo1.publishLocal  
./mill foo2.mimaReportBinaryIssues
1 Like

Such are the perils of mixin composition.

Concrete D was not recompiled with the changed Child:

Exception in thread "main" java.lang.AbstractMethodError: Receiver class D does not define or inherit an implementation of the resolved method 'abstract int Child$$super$f()' of interface Child.
        at Child.f(superb.scala:6)
        at Child.f$(superb.scala:5)
        at D.f(impl.scala:2)
        at impl$package$.test(impl.scala:5)
        at test.main(impl.scala:4)

The concrete class is responsible for implementing the super call, however it is resolved.

A similar thing happens with outer accessors.

trait Generation:
  def g = 27

  trait Parent:
    def f = 42

  trait Child extends Parent:
    override def f = super.f + g

@main def main = println:
  val g = new Generation {}
  class C extends g.Child
  C().f

where Child is

    trait Child() extends Object, Generation.this.Parent {
      def Generation$Child$$super$f(): Int
      override def f(): Int =
        this.Generation$Child$$super$f() + this.Generation$Child$$$outer().g()
      final def Generation$Child$$$outer(): Generation
    }

and C is

          class C($outer: superb$package) extends Object, Generation.this.Child
             {
            super()
            override def f(): Int = super[Child].f()
            def Generation$Child$$super$f(): Int = super[Parent].f()
            private val $outer: superb$package
            final def superb$package$_$C$$$outer(): superb$package =
              C.this.$outer
            final def Generation$Parent$$$outer(): Generation = g
            final def Generation$Child$$$outer(): Generation = g
          }

Edit: Josh Bloch calls it brittle inheritance. I wonder if it is worth a lint, so you know before mima tells you that you broke it.

2 Likes