Skip to content

Commit 2cce916

Browse files
committed
Merge pull request scala-js#2141 from sjrd/default-methods
Fix scala-js#1269: Add support for default methods.
2 parents 4e71cc2 + eef5c3e commit 2cce916

File tree

16 files changed

+548
-90
lines changed

16 files changed

+548
-90
lines changed

compiler/src/main/scala/org/scalajs/core/compiler/GenJSCode.scala

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -691,13 +691,12 @@ abstract class GenJSCode extends plugins.PluginComponent
691691
* * Primitives, since they are never actually called
692692
* * Abstract methods
693693
* * Constructors of hijacked classes
694-
* * Trivial constructors, which only call their super constructor, with
695-
* the same signature, and the same arguments. The JVM needs these
696-
* constructors, but not JavaScript. Since there are lots of them, we
697-
* take the trouble of recognizing and removing them.
694+
* * Methods with the {{{@JavaDefaultMethod}}} annotation mixed in classes.
698695
*
699-
* Constructors are emitted by generating their body as a statement, then
700-
* return `this`.
696+
* Constructors are emitted by generating their body as a statement.
697+
*
698+
* Interface methods with the {{{@JavaDefaultMethod}}} annotation produce
699+
* default methods forwarding to the trait impl class method.
701700
*
702701
* Other (normal) methods are emitted with `genMethodBody()`.
703702
*/
@@ -734,13 +733,36 @@ abstract class GenJSCode extends plugins.PluginComponent
734733
if (scalaPrimitives.isPrimitive(sym)) {
735734
None
736735
} else if (sym.isDeferred || sym.owner.isInterface) {
736+
val body = if (sym.hasAnnotation(JavaDefaultMethodAnnotation)) {
737+
/* For an interface method with @JavaDefaultMethod, make it a
738+
* default method calling the impl class method.
739+
*/
740+
val implClassSym = sym.owner.implClass
741+
val implMethodSym = implClassSym.info.member(sym.name).suchThat { s =>
742+
s.isMethod &&
743+
s.tpe.params.size == sym.tpe.params.size + 1 &&
744+
s.tpe.params.head.tpe =:= sym.owner.toTypeConstructor &&
745+
s.tpe.params.tail.zip(sym.tpe.params).forall {
746+
case (sParam, symParam) =>
747+
sParam.tpe =:= symParam.tpe
748+
}
749+
}
750+
genTraitImplApply(implMethodSym,
751+
js.This()(currentClassType) :: jsParams.map(_.ref))
752+
} else {
753+
js.EmptyTree
754+
}
737755
Some(js.MethodDef(static = false, methodName,
738-
jsParams, currentClassType, js.EmptyTree)(
756+
jsParams, toIRType(sym.tpe.resultType), body)(
739757
OptimizerHints.empty, None))
740758
} else if (isRawJSCtorDefaultParam(sym)) {
741759
None
742760
} else if (sym.isClassConstructor && isHijackedBoxedClass(sym.owner)) {
743761
None
762+
} else if (!sym.owner.isImplClass &&
763+
sym.hasAnnotation(JavaDefaultMethodAnnotation)) {
764+
// Do not emit trait impl forwarders with @JavaDefaultMethod
765+
None
744766
} else {
745767
withScopedVars(
746768
mutableLocalVars := mutable.Set.empty,

compiler/src/main/scala/org/scalajs/core/compiler/JSDefinitions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ trait JSDefinitions { self: JSGlobalAddons =>
6464
lazy val JSExportNamedAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSExportNamed")
6565
lazy val ScalaJSDefinedAnnotation = getRequiredClass("scala.scalajs.js.annotation.ScalaJSDefined")
6666

67+
lazy val JavaDefaultMethodAnnotation = getRequiredClass("scala.scalajs.js.annotation.JavaDefaultMethod")
68+
6769
lazy val JSAnyTpe = JSAnyClass.toTypeConstructor
6870
lazy val JSObjectTpe = JSObjectClass.toTypeConstructor
6971

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package java.util
22

3+
import scala.scalajs.js.annotation.JavaDefaultMethod
4+
35
// scalastyle:off equals.hash.code
46

57
trait Comparator[A] {
68
def compare(o1: A, o2: A): Int
79
def equals(obj: Any): Boolean
10+
11+
@JavaDefaultMethod
12+
def reversed(): Comparator[A] =
13+
Collections.reverseOrder(this)
814
}

js-envs/src/main/scala/org/scalajs/jsenv/rhino/ScalaJSCoreLib.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class ScalaJSCoreLib private[rhino] (semantics: Semantics,
134134
Info("c"),
135135
Info("h"),
136136
Info("s", isStatics = true),
137+
Info("f", isStatics = true),
137138
Info("n"),
138139
Info("m"),
139140
Info("is"),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* __ *\
2+
** ________ ___ / / ___ __ ____ Scala.js API **
3+
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2015, LAMP/EPFL **
4+
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
5+
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
6+
** |/____/ **
7+
\* */
8+
9+
10+
package scala.scalajs.js.annotation
11+
12+
/** Mark a concrete trait method as a Java default method.
13+
*
14+
* This annotation can be used on concrete trait methods to mark them as
15+
* Java default methods. This should be used *only* to implement interfaces
16+
* of the JDK that have default methods in Java.
17+
*
18+
* Otherwise using this annotation is unspecified.
19+
*/
20+
class JavaDefaultMethod extends scala.annotation.StaticAnnotation
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* __ *\
2+
** ________ ___ / / ___ __ ____ Scala.js Test Suite **
3+
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2015, LAMP/EPFL **
4+
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
5+
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
6+
** |/____/ **
7+
\* */
8+
package org.scalajs.testsuite.compiler
9+
10+
import org.junit.Test
11+
import org.junit.Assert._
12+
13+
import java.{util => ju}
14+
15+
class DefaultMethodsTest {
16+
17+
@Test def canOverrideDefaultMethod(): Unit = {
18+
var counter = 0
19+
20+
class SpecialIntComparator extends ju.Comparator[Int] {
21+
def compare(o1: Int, o2: Int): Int =
22+
o1.compareTo(o2)
23+
24+
override def reversed(): ju.Comparator[Int] = {
25+
counter += 1
26+
super.reversed()
27+
}
28+
}
29+
30+
val c = new SpecialIntComparator
31+
assertTrue(c.compare(5, 7) < 0)
32+
assertEquals(0, counter)
33+
34+
val reversed = c.reversed()
35+
assertEquals(1, counter)
36+
assertTrue(reversed.compare(5, 7) > 0)
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* __ *\
2+
** ________ ___ / / ___ __ ____ Scala.js Test Suite **
3+
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2015, LAMP/EPFL **
4+
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
5+
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
6+
** |/____/ **
7+
\* */
8+
package org.scalajs.testsuite.javalib.util
9+
10+
import org.junit.Test
11+
import org.junit.Assert._
12+
13+
import java.{util => ju}
14+
15+
class ComparatorTestOnJDK8 {
16+
17+
@Test def reversed(): Unit = {
18+
class IntComparator extends ju.Comparator[Int] {
19+
def compare(a: Int, b: Int): Int = {
20+
/* Using Int.MinValue makes sure that Comparator.reversed() does not
21+
* use the naive implementation of negating the original comparator's
22+
* result.
23+
*/
24+
if (a == b) 0
25+
else if (a < b) Int.MinValue
26+
else Int.MaxValue
27+
}
28+
}
29+
30+
val comparator = new IntComparator
31+
val reversed = comparator.reversed()
32+
33+
assertEquals(0, reversed.compare(5, 5))
34+
assertTrue(reversed.compare(3, 1) < 0)
35+
assertTrue(reversed.compare(6, 8) > 0)
36+
}
37+
38+
}

tools/scalajsenv.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ ScalaJS.d = {}; // Data for types
117117
ScalaJS.c = {}; // Scala.js constructors
118118
ScalaJS.h = {}; // Inheritable constructors (without initialization code)
119119
ScalaJS.s = {}; // Static methods
120+
ScalaJS.f = {}; // Default methods
120121
ScalaJS.n = {}; // Module instances
121122
ScalaJS.m = {}; // Module accessors
122123
ScalaJS.is = {}; // isInstanceOf methods

tools/shared/src/main/scala/org/scalajs/core/tools/javascript/JSDesugaring.scala

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,8 +1576,24 @@ private[javascript] object JSDesugaring {
15761576
}
15771577

15781578
case ApplyStatically(receiver, cls, method, args) =>
1579-
val fun = encodeClassVar(cls.className).prototype DOT method
1580-
js.Apply(fun DOT "call", (receiver :: args) map transformExpr)
1579+
val className = cls.className
1580+
val transformedArgs = (receiver :: args) map transformExpr
1581+
1582+
if (classEmitter.isInterface(className)) {
1583+
val Ident(methodName, origName) = method
1584+
if (isStrongMode) {
1585+
js.Apply(js.DotSelect(envField("c", className),
1586+
js.Ident("$f_" + methodName, origName)(method.pos)),
1587+
transformedArgs)
1588+
} else {
1589+
val fullName = className + "__" + methodName
1590+
js.Apply(envField("f", fullName, origName),
1591+
transformedArgs)
1592+
}
1593+
} else {
1594+
val fun = encodeClassVar(className).prototype DOT method
1595+
js.Apply(fun DOT "call", transformedArgs)
1596+
}
15811597

15821598
case ApplyStatic(cls, method, args) =>
15831599
if (isStrongMode) {

tools/shared/src/main/scala/org/scalajs/core/tools/javascript/ScalaJSClassEmitter.scala

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,31 +39,33 @@ final class ScalaJSClassEmitter private (
3939
this(semantics, outputMode, linkingUnit, linkingUnit.globalInfo)
4040
}
4141

42-
@deprecated(
43-
"This constructor creates an emitter that cannot handle JS classes. " +
44-
"Use the constructor with a LinkingUnit instead.", "0.6.5")
45-
def this(semantics: Semantics, outputMode: OutputMode,
46-
globalInfo: LinkingUnit.GlobalInfo) =
47-
this(semantics, OutputMode.ECMAScript51Global, null, globalInfo)
48-
49-
@deprecated(
50-
"This constructor creates an emitter that cannot handle JS classes. " +
51-
"Use the constructor with a LinkingUnit instead.", "0.6.2")
52-
def this(semantics: Semantics, globalInfo: LinkingUnit.GlobalInfo) =
53-
this(semantics, OutputMode.ECMAScript51Global, globalInfo)
54-
55-
@deprecated(
56-
"This constructor creates an emitter that cannot handle JS classes. " +
57-
"Use the constructor with a LinkingUnit instead.", "0.6.1")
58-
def this(semantics: Semantics) =
59-
this(semantics, LinkingUnit.GlobalInfo.SafeApproximation)
60-
61-
private[javascript] lazy val linkedClassByName: Map[String, LinkedClass] = {
62-
if (linkingUnit == null) {
63-
throw new IllegalArgumentException(
64-
"A class emitter created without a LinkingUnit cannot emit JS classes")
65-
}
42+
private[javascript] lazy val linkedClassByName: Map[String, LinkedClass] =
6643
linkingUnit.classDefs.map(c => c.encodedName -> c).toMap
44+
45+
private[javascript] def isInterface(className: String): Boolean = {
46+
/* TODO In theory, there is a flaw in the incremental behavior about this.
47+
*
48+
* This method is used to desugar ApplyStatically nodes. Depending on
49+
* whether className is a class or an interface, the desugaring changes.
50+
* This means that the result of desugaring an ApplyStatically depends on
51+
* some global knowledge from the whole program (and not only of the method
52+
* being desugared). If, from one run to the next, className switches from
53+
* being a class to an interface or vice versa, there is nothing demanding
54+
* that the IR of the call site change, yet the desugaring should change.
55+
* In theory, this causes a flaw in the incremental behavior of the
56+
* Emitter, which will not invalidate its cache for this.
57+
*
58+
* In practice, this should not happen, though. A method can only be called
59+
* statically by subclasses and subtraits at the Scala *language* level.
60+
* We also know that when a class/trait changes, all its subclasses and
61+
* subtraits are recompiled by sbt's incremental compilation. This should
62+
* mean that the input IR is always changed in that case anyway.
63+
*
64+
* It would be good to fix thoroughly if the Emitter/ScalaJSClassEmitter
65+
* gets something for incremental whole-program updates, but for now, we
66+
* live with the theoretical flaw.
67+
*/
68+
linkedClassByName(className).kind == ClassKind.Interface
6769
}
6870

6971
private implicit def implicitOutputMode: OutputMode = outputMode
@@ -94,6 +96,8 @@ final class ScalaJSClassEmitter private (
9496
var reverseParts: List[js.Tree] = Nil
9597

9698
reverseParts ::= genStaticMembers(tree)
99+
if (kind == ClassKind.Interface)
100+
reverseParts ::= genDefaultMethods(tree)
97101
if (kind.isAnyScalaJSDefinedClass && tree.hasInstances)
98102
reverseParts ::= genClass(tree)
99103
if (needInstanceTests(tree)) {
@@ -118,6 +122,13 @@ final class ScalaJSClassEmitter private (
118122
js.Block(staticMemberDefs)(tree.pos)
119123
}
120124

125+
def genDefaultMethods(tree: LinkedClass): js.Tree = {
126+
val className = tree.name.name
127+
val defaultMethodDefs =
128+
tree.memberMethods.map(m => genDefaultMethod(className, m.tree))
129+
js.Block(defaultMethodDefs)(tree.pos)
130+
}
131+
121132
def genClass(tree: LinkedClass): js.Tree = {
122133
val className = tree.name.name
123134
val typeFunctionDef = genConstructor(tree)
@@ -355,6 +366,37 @@ final class ScalaJSClassEmitter private (
355366
}
356367
}
357368

369+
/** Generates a default method. */
370+
def genDefaultMethod(className: String, method: MethodDef): js.Tree = {
371+
implicit val pos = method.pos
372+
373+
/* TODO The identifier `$thiz` cannot be produced by 0.6.x compilers due to
374+
* their name mangling, which guarantees that it is unique. We should find
375+
* a better way to do this in the future, though.
376+
*/
377+
val thisIdent = js.Ident("$thiz", Some("this"))
378+
379+
val methodFun0 = desugarToFunction(this, className, Some(thisIdent),
380+
method.args, method.body, method.resultType == NoType)
381+
382+
val methodFun = js.Function(
383+
js.ParamDef(thisIdent, rest = false) :: methodFun0.args,
384+
methodFun0.body)(methodFun0.pos)
385+
386+
val Ident(methodName, origName) = method.name
387+
388+
outputMode match {
389+
case OutputMode.ECMAScript6StrongMode =>
390+
val propName = js.Ident("$f_" + methodName, origName)(method.name.pos)
391+
js.MethodDef(static = true, propName, methodFun.args, methodFun.body)
392+
393+
case _ =>
394+
envFieldDef(
395+
"f", className + "__" + methodName, origName,
396+
methodFun)
397+
}
398+
}
399+
358400
/** Generates a property. */
359401
def genProperty(className: String, property: PropertyDef): js.Tree = {
360402
outputMode match {

0 commit comments

Comments
 (0)