From 72282bc23c47eb18f265ca6e1bf30eec2a47b1c4 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 6 Aug 2023 18:24:20 +0200 Subject: [PATCH] Force super classes to generate default bridges on lookup - Fixes #2520 (make default bridge generation smarter) - Avoids traversal order dependent behavior. Since we potentially need to do a default target check on every method lookup, we memoize calculated default targets. Note that this might generate more default bridges than actually necessary. However, since they will not be marked as reachable, they will not even be synthesized. --- .../scalajs/linker/analyzer/Analyzer.scala | 64 +++++++---------- .../org/scalajs/linker/BaseLinkerTest.scala | 72 +++++++++++++++++++ 2 files changed, 99 insertions(+), 37 deletions(-) create mode 100644 linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala index 1db3f58e3b..23164de999 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala @@ -667,50 +667,40 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean, assert(isScalaClass || isInterface, s"Cannot call lookupMethod($methodName) on non Scala class $this") - @tailrec - def tryLookupInherited(ancestorInfo: ClassInfo): Option[MethodInfo] = { - ancestorInfo.publicMethodInfos.get(methodName) match { - case Some(m) if !m.isAbstract && (!m.nonExistent || ancestorInfo == this) => - Some(m) - case _ => - ancestorInfo.superClass match { - case Some(superClass) => tryLookupInherited(superClass) - case None => None + publicMethodInfos.get(methodName) match { + case Some(m) if !m.isAbstract => Some(m) + + case _ => + val candidate = superClass + .flatMap(_.tryLookupMethod(methodName)) + .filterNot(_.nonExistent) + + if (allowAddingSyntheticMethods) { + def maybeDefaultTarget = getDefaultTarget(methodName) + + def needsDefaultOverride(method: MethodInfo): Boolean = { + /* The .get is OK, since we only get here if: + * - This class doesn't implement the method directly. + * - The superClass has found a default target. + * In this case, we always find at least one target. + */ + method.isDefaultBridge && method.defaultBridgeTarget != maybeDefaultTarget.get.owner.className } - } - } - val existing = - if (isScalaClass) tryLookupInherited(this) - else publicMethodInfos.get(methodName).filter(!_.isAbstract) - if (!allowAddingSyntheticMethods) { - existing - } else if (existing.exists(m => !m.isDefaultBridge || m.owner == this)) { - /* If we found a non-bridge, it must be the right target. - * If we found a bridge directly in this class/interface, it must also - * be the right target. - */ - existing - } else { - // Try and find the target of a possible default bridge - findDefaultTarget(methodName).fold { - assert(existing.isEmpty) - existing - } { defaultTarget => - if (existing.exists(_.defaultBridgeTarget == defaultTarget.owner.className)) { - /* If we found an existing bridge targeting the right method, we - * can reuse it. - * We also get here with None when there is no target whatsoever. - */ - existing + candidate + .filterNot(needsDefaultOverride(_)) + .orElse(maybeDefaultTarget.map(createDefaultBridge(_))) } else { - // Otherwise, create a new default bridge - Some(createDefaultBridge(defaultTarget)) + candidate } - } } } + private val defaultTargets = mutable.Map.empty[MethodName, Option[MethodInfo]] + + private def getDefaultTarget(methodName: MethodName): Option[MethodInfo] = + defaultTargets.getOrElseUpdate(methodName, findDefaultTarget(methodName)) + /** Resolves an inherited default method. * * This lookup is specified by the JVM resolution rules for default diff --git a/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala new file mode 100644 index 0000000000..b6870d16e5 --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala @@ -0,0 +1,72 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.ir.ClassKind +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ + +import org.scalajs.junit.async._ + +import org.scalajs.linker.interface.StandardConfig +import org.scalajs.linker.standard._ + +import org.scalajs.linker.testutils.TestIRBuilder._ +import org.scalajs.linker.testutils.LinkingUtils._ + +class BaseLinkerTest { + import scala.concurrent.ExecutionContext.Implicits.global + + @Test + def noUnnecessaryDefaultBridges(): AsyncResult = await { + val fooName = m("foo", Nil, IntRef) + val classDefs = Seq( + classDef( + "Intf", + kind = ClassKind.Interface, + methods = List( + MethodDef(EMF, fooName, NON, Nil, IntType, Some(int(1)))(EOH, UNV)) + ), + classDef( + "Base", + kind = ClassKind.Class, + superClass = Some(ObjectClass), + interfaces = List("Intf"), + methods = List(trivialCtor("Base")) + ), + classDef( + "Sub", + kind = ClassKind.Class, + superClass = Some("Base"), + methods = List(trivialCtor("Sub")) + ), + mainTestClassDef( + consoleLog(Apply(EAF, New("Sub", NoArgConstructorName, Nil), fooName, Nil)(IntType)) + ) + ) + + val config = StandardConfig().withOptimizer(false) + + for (moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers, config = config)) yield { + val clazz = findClass(moduleSet, "Sub").get + assertFalse(clazz.methods.exists(_.name.name == fooName)) + } + } + + private def findClass(moduleSet: ModuleSet, name: ClassName): Option[LinkedClass] = + moduleSet.modules.flatMap(_.classDefs).find(_.className == name) +}