Skip to content

Commit 3ebbd17

Browse files
committed
Synthesize reflective proxies at link time.
Instead of relying on reflective proxies stored in sjsir files by the compiler, we synthesize them at link time, on demand. During deserialization of sjsir files, we throw away all the reflective proxies. The Analyzer identifies the reflective proxies needed to satisfy calls of reflective methods, and the Linker synthesizes the bridges. In this commit, the compiler is not modified and still emits the reflective proxies, so that we can test the backward binary compatibility hack.
1 parent ac65737 commit 3ebbd17

File tree

7 files changed

+247
-27
lines changed

7 files changed

+247
-27
lines changed

ir/src/main/scala/org/scalajs/core/ir/InfoSerializers.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ object InfoSerializers {
101101

102102
import input._
103103

104+
val useHacks065 =
105+
Set("0.6.0", "0.6.3", "0.6.4", "0.6.5").contains(version)
106+
104107
val encodedName = readUTF()
105108
val isExported = readBoolean()
106109
val kind = ClassKind.fromByte(readByte())
@@ -126,7 +129,12 @@ object InfoSerializers {
126129
accessedClassData)
127130
}
128131

129-
val methods = readList(readMethod())
132+
val methods0 = readList(readMethod())
133+
val methods = if (true) { // useHacks065
134+
methods0.filter(m => !Definitions.isReflProxyName(m.encodedName))
135+
} else {
136+
methods0
137+
}
130138

131139
val info = ClassInfo(encodedName, isExported, kind,
132140
superClass, interfaces, methods)

ir/src/main/scala/org/scalajs/core/ir/Serializers.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,17 @@ object Serializers {
712712
val superClass = readOptIdent()
713713
val parents = readIdents()
714714
val jsName = Some(readString()).filter(_ != "")
715-
val defs = readTrees()
715+
val defs0 = readTrees()
716+
val defs = if (true) { // useHacks065
717+
defs0.filter {
718+
case MethodDef(_, Ident(name, _), _, _, _) =>
719+
!Definitions.isReflProxyName(name)
720+
case _ =>
721+
true
722+
}
723+
} else {
724+
defs0
725+
}
716726
val optimizerHints = new OptimizerHints(readInt())
717727
ClassDef(name, kind, superClass, parents, jsName, defs)(optimizerHints)
718728

test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,24 @@ object ReflectiveCallTest extends JasmineTest {
221221
expect(call(new C)).toEqual(1)
222222
}
223223

224+
it("should be bug-compatible with Scala/JVM for inherited overloads") {
225+
class Base {
226+
def foo(x: Option[Int]): String = "a"
227+
}
228+
229+
class Sub extends Base {
230+
def foo(x: Option[String]): Int = 1
231+
}
232+
233+
val sub = new Sub
234+
235+
val x: { def foo(x: Option[Int]): Any } = sub
236+
expect(x.foo(Some(1)).asInstanceOf[js.Any]).toEqual(1) // here is the "bug"
237+
238+
val y: { def foo(x: Option[String]): Any } = sub
239+
expect(y.foo(Some("hello")).asInstanceOf[js.Any]).toEqual(1)
240+
}
241+
224242
it("should work on java.lang.Object.{ notify, notifyAll } - #303") {
225243
type ObjNotifyLike = Any {
226244
def notify(): Unit

tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/Analysis.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ object Analysis {
7575
final case object None extends MethodSyntheticKind
7676
// TODO Get rid of InheritedConstructor when we can break binary compat
7777
final case object InheritedConstructor extends MethodSyntheticKind
78+
final case class ReflectiveProxy(target: String) extends MethodSyntheticKind
7879
}
7980

8081
sealed trait Error {

tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/Analyzer.scala

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,24 @@ import scala.collection.mutable
1515

1616
import org.scalajs.core.ir
1717
import ir.{ClassKind, Definitions, Infos}
18-
import Definitions.{isConstructorName, isReflProxyName}
18+
import Definitions._
1919

2020
import org.scalajs.core.tools.sem._
2121
import org.scalajs.core.tools.javascript.{LongImpl, OutputMode}
2222

2323
import ScalaJSOptimizer._
2424

2525
final class Analyzer(semantics: Semantics, outputMode: OutputMode,
26-
reachOptimizerSymbols: Boolean, initialLink: Boolean) extends Analysis {
26+
reachOptimizerSymbols: Boolean,
27+
allowAddingSyntheticMethods: Boolean) extends Analysis {
2728
import Analyzer._
2829
import Analysis._
2930

30-
@deprecated("Use the overload with an explicit `initialLink` flag", "0.6.6")
31+
@deprecated("Use the overload with an explicit `allowAddingSyntheticMethods` flag", "0.6.6")
3132
def this(semantics: Semantics, outputMode: OutputMode,
3233
reachOptimizerSymbols: Boolean) = {
33-
this(semantics, outputMode, reachOptimizerSymbols, initialLink = true)
34+
this(semantics, outputMode, reachOptimizerSymbols,
35+
allowAddingSyntheticMethods = true)
3436
}
3537

3638
@deprecated("Use the overload with an explicit output mode", "0.6.3")
@@ -297,7 +299,7 @@ final class Analyzer(semantics: Semantics, outputMode: OutputMode,
297299
* during the initial link. In a refiner, this must not happen anymore.
298300
*/
299301
methodInfos.get(ctorName).getOrElse {
300-
if (!initialLink) {
302+
if (!allowAddingSyntheticMethods) {
301303
createNonExistentMethod(ctorName)
302304
} else {
303305
val inherited = lookupMethod(ctorName)
@@ -350,6 +352,139 @@ final class Analyzer(semantics: Semantics, outputMode: OutputMode,
350352
loop(this)
351353
}
352354

355+
def tryLookupReflProxyMethod(proxyName: String): Option[MethodInfo] = {
356+
if (!allowAddingSyntheticMethods) {
357+
tryLookupMethod(proxyName)
358+
} else {
359+
/* The lookup for a target method in this code implements the
360+
* algorithm defining `java.lang.Class.getMethod`. This mimics how
361+
* reflective calls are implemented on the JVM, at link time.
362+
*
363+
* Caveat: protected methods are not ignored. This can only make an
364+
* otherwise invalid reflective call suddenly able to call a protected
365+
* method. It never breaks valid reflective calls. This could be fixed
366+
* if the IR retained the information that a method is protected.
367+
*/
368+
369+
@tailrec
370+
def loop(ancestorInfo: ClassInfo): Option[MethodInfo] = {
371+
if (ancestorInfo ne null) {
372+
ancestorInfo.methodInfos.get(proxyName) match {
373+
case Some(m) =>
374+
assert(m.isReflProxy && !m.isAbstract)
375+
Some(m)
376+
377+
case _ =>
378+
ancestorInfo.findProxyMatch(proxyName) match {
379+
case Some(target) =>
380+
val targetName = target.encodedName
381+
Some(ancestorInfo.createReflProxy(proxyName, targetName))
382+
383+
case None =>
384+
loop(ancestorInfo.superClass)
385+
}
386+
}
387+
} else {
388+
None
389+
}
390+
}
391+
392+
loop(this)
393+
}
394+
}
395+
396+
private def findProxyMatch(proxyName: String): Option[MethodInfo] = {
397+
val candidates = methodInfos.valuesIterator.filter { m =>
398+
// TODO In theory we should filter out protected methods
399+
!m.isReflProxy && !m.isExported && !m.isAbstract &&
400+
reflProxyMatches(m.encodedName, proxyName)
401+
}.toSeq
402+
403+
/* From the JavaDoc of java.lang.Class.getMethod:
404+
*
405+
* If more than one [candidate] method is found in C, and one of these
406+
* methods has a return type that is more specific than any of the
407+
* others, that method is reflected; otherwise one of the methods is
408+
* chosen arbitrarily.
409+
*/
410+
411+
val targets = candidates.filterNot { c =>
412+
val resultType = methodResultType(c.encodedName)
413+
candidates.exists { other =>
414+
(other ne c) &&
415+
isMoreSpecific(methodResultType(other.encodedName), resultType)
416+
}
417+
}
418+
419+
/* This last step (chosen arbitrarily) causes some soundness issues of
420+
* the implementation of reflective calls. This is bug-compatible with
421+
* Scala/JVM.
422+
*/
423+
targets.headOption
424+
}
425+
426+
private def reflProxyMatches(methodName: String, proxyName: String): Boolean = {
427+
val sepPos = methodName.lastIndexOf("__")
428+
sepPos >= 0 && methodName.substring(0, sepPos + 2) == proxyName
429+
}
430+
431+
private def methodResultType(methodName: String): ir.Types.ReferenceType = {
432+
val typeName = methodName.substring(methodName.lastIndexOf("__") + 2)
433+
val arrayDepth = typeName.indexWhere(_ != 'A')
434+
if (arrayDepth == 0)
435+
ir.Types.ClassType(typeName)
436+
else
437+
ir.Types.ArrayType(typeName.substring(arrayDepth), arrayDepth)
438+
}
439+
440+
private def isMoreSpecific(left: ir.Types.ReferenceType,
441+
right: ir.Types.ReferenceType): Boolean = {
442+
import ir.Types._
443+
444+
def classIsMoreSpecific(leftCls: String, rightCls: String): Boolean = {
445+
leftCls != rightCls && {
446+
val leftInfo = _classInfos.get(leftCls)
447+
val rightInfo = _classInfos.get(rightCls)
448+
leftInfo.zip(rightInfo).exists { case (l, r) =>
449+
l.ancestors.contains(r)
450+
}
451+
}
452+
}
453+
454+
(left, right) match {
455+
case (ClassType(leftCls), ClassType(rightCls)) =>
456+
classIsMoreSpecific(leftCls, rightCls)
457+
case (ArrayType(leftBase, leftDepth), ArrayType(rightBase, rightDepth)) =>
458+
leftDepth == rightDepth && classIsMoreSpecific(leftBase, rightBase)
459+
case (ArrayType(_, _), ClassType(ObjectClass)) =>
460+
true
461+
case _ =>
462+
false
463+
}
464+
}
465+
466+
private def createReflProxy(proxyName: String,
467+
targetName: String): MethodInfo = {
468+
assert(this.isScalaClass,
469+
s"Cannot create reflective proxy in non-Scala class $this")
470+
471+
val returnsChar = targetName.endsWith("__C")
472+
val syntheticInfo = Infos.MethodInfo(
473+
encodedName = proxyName,
474+
methodsCalled = Map(
475+
this.encodedName -> List(targetName)),
476+
methodsCalledStatically = (
477+
if (returnsChar) Map(BoxedCharacterClass -> List("init___C"))
478+
else Map.empty),
479+
instantiatedClasses = (
480+
if (returnsChar) List(BoxedCharacterClass)
481+
else Nil))
482+
val m = new MethodInfo(this, syntheticInfo)
483+
m.syntheticKind = MethodSyntheticKind.ReflectiveProxy(targetName)
484+
methodInfos += proxyName -> m
485+
m
486+
}
487+
353488
def lookupStaticMethod(methodName: String): MethodInfo = {
354489
tryLookupStaticMethod(methodName).getOrElse {
355490
val syntheticData = createMissingMethodInfo(methodName, isStatic = true)
@@ -473,7 +608,7 @@ final class Analyzer(semantics: Semantics, outputMode: OutputMode,
473608

474609
private def delayedCallMethod(methodName: String)(implicit from: From): Unit = {
475610
if (isReflProxyName(methodName)) {
476-
tryLookupMethod(methodName).foreach(_.reach(this))
611+
tryLookupReflProxyMethod(methodName).foreach(_.reach(this))
477612
} else {
478613
lookupMethod(methodName).reach(this)
479614
}

tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/Linker.scala

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ final class Linker(semantics: Semantics, outputMode: OutputMode,
9595

9696
val analysis = logTime(logger, "Linker: Compute reachability") {
9797
val analyzer = new Analyzer(semantics, outputMode, reachOptimizerSymbols,
98-
initialLink = true)
98+
allowAddingSyntheticMethods = true)
9999
analyzer.computeReachability(infoInput)
100100
}
101101

@@ -236,6 +236,11 @@ final class Linker(semantics: Semantics, outputMode: OutputMode,
236236
val syntheticMDef = synthesizeInheritedConstructor(
237237
analyzerInfo, m, getTree, analysis)(classDef.pos)
238238
memberMethods += linkedSyntheticMethod(syntheticMDef)
239+
240+
case MethodSyntheticKind.ReflectiveProxy(targetName) =>
241+
val syntheticMDef = synthesizeReflectiveProxy(
242+
analyzerInfo, m, targetName, getTree, analysis)
243+
memberMethods += linkedSyntheticMethod(syntheticMDef)
239244
}
240245
}
241246

@@ -276,23 +281,8 @@ final class Linker(semantics: Semantics, outputMode: OutputMode,
276281
implicit pos: Position): MethodDef = {
277282
val encodedName = methodInfo.encodedName
278283

279-
@tailrec
280-
def findInheritedMethodDef(ancestorInfo: Analysis.ClassInfo): MethodDef = {
281-
val inherited = ancestorInfo.methodInfos(methodInfo.encodedName)
282-
if (inherited.syntheticKind == MethodSyntheticKind.None) {
283-
val (classDef, _) = getTree(ancestorInfo.encodedName)
284-
classDef.defs.collectFirst {
285-
case mDef: MethodDef if mDef.name.name == encodedName => mDef
286-
}.getOrElse {
287-
throw new AssertionError(
288-
s"Cannot find $encodedName in ${ancestorInfo.encodedName}")
289-
}
290-
} else {
291-
findInheritedMethodDef(ancestorInfo.superClass)
292-
}
293-
}
294-
295-
val inheritedMDef = findInheritedMethodDef(classInfo.superClass)
284+
val inheritedMDef = findInheritedMethodDef(classInfo.superClass,
285+
encodedName, getTree, _.syntheticKind == MethodSyntheticKind.None)
296286

297287
val origName = inheritedMDef.name.asInstanceOf[Ident].originalName
298288
val ctorIdent = Ident(encodedName, origName)
@@ -307,6 +297,64 @@ final class Linker(semantics: Semantics, outputMode: OutputMode,
307297
inheritedMDef.hash) // over-approximation
308298
}
309299

300+
private def synthesizeReflectiveProxy(
301+
classInfo: Analysis.ClassInfo, methodInfo: Analysis.MethodInfo,
302+
targetName: String, getTree: TreeProvider,
303+
analysis: Analysis): MethodDef = {
304+
val encodedName = methodInfo.encodedName
305+
306+
val targetMDef = findInheritedMethodDef(classInfo, targetName, getTree)
307+
308+
implicit val pos = targetMDef.pos
309+
310+
val targetIdent = targetMDef.name.asInstanceOf[Ident].copy() // for the new pos
311+
val proxyIdent = Ident(encodedName, None)
312+
val params = targetMDef.args.map(_.copy()) // for the new pos
313+
val currentClassType = ClassType(classInfo.encodedName)
314+
315+
val call = Apply(This()(currentClassType),
316+
targetIdent, params.map(_.ref))(targetMDef.resultType)
317+
318+
val body = if (targetName.endsWith("__C")) {
319+
// A Char needs to be boxed
320+
New(ClassType(Definitions.BoxedCharacterClass),
321+
Ident("init___C"), List(call))
322+
} else if (targetName.endsWith("__V")) {
323+
// Materialize an `undefined` result for void methods
324+
Block(call, Undefined())
325+
} else {
326+
call
327+
}
328+
329+
MethodDef(static = false, proxyIdent, params, AnyType, body)(
330+
OptimizerHints.empty, targetMDef.hash)
331+
}
332+
333+
private def findInheritedMethodDef(classInfo: Analysis.ClassInfo,
334+
methodName: String, getTree: TreeProvider,
335+
p: Analysis.MethodInfo => Boolean = _ => true): MethodDef = {
336+
@tailrec
337+
def loop(ancestorInfo: Analysis.ClassInfo): MethodDef = {
338+
assert(ancestorInfo != null,
339+
s"Could not find $methodName anywhere in ${classInfo.encodedName}")
340+
341+
val inherited = ancestorInfo.methodInfos.get(methodName)
342+
if (inherited.exists(p)) {
343+
val (classDef, _) = getTree(ancestorInfo.encodedName)
344+
classDef.defs.collectFirst {
345+
case mDef: MethodDef if mDef.name.name == methodName => mDef
346+
}.getOrElse {
347+
throw new AssertionError(
348+
s"Cannot find $methodName in ${ancestorInfo.encodedName}")
349+
}
350+
} else {
351+
loop(ancestorInfo.superClass)
352+
}
353+
}
354+
355+
loop(classInfo)
356+
}
357+
310358
private def startRun(): Unit = {
311359
statsReused = 0
312360
statsInvalidated = 0

tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/Refiner.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class Refiner(semantics: Semantics, outputMode: OutputMode) {
1616
def refine(unit: LinkingUnit, logger: Logger): LinkingUnit = {
1717
val analysis = logTime(logger, "Refiner: Compute reachability") {
1818
val analyzer = new Analyzer(semantics, outputMode,
19-
reachOptimizerSymbols = false, initialLink = false)
19+
reachOptimizerSymbols = false, allowAddingSyntheticMethods = false)
2020
analyzer.computeReachability(unit.infos.values.toList)
2121
}
2222

0 commit comments

Comments
 (0)