Skip to content

Commit 4e1bb05

Browse files
committed
Scala.js: Generalize js.Function's to allow user-defined ones.
Forward port of the upstream commit scala-js/scala-js@7edacf5
1 parent 1deaede commit 4e1bb05

File tree

10 files changed

+170
-49
lines changed

10 files changed

+170
-49
lines changed

compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2702,7 +2702,7 @@ class JSCodeGen()(using genCtx: Context) {
27022702

27032703
case JSCallingConvention.Call =>
27042704
requireNotSuper()
2705-
if (jsdefn.isJSThisFunctionClass(sym.owner))
2705+
if (sym.owner.isSubClass(jsdefn.JSThisFunctionClass))
27062706
js.JSMethodApply(ruleOutGlobalScope(receiver), js.StringLiteral("call"), args)
27072707
else
27082708
js.JSFunctionApply(ruleOutGlobalScope(receiver), args)
@@ -3052,17 +3052,35 @@ class JSCodeGen()(using genCtx: Context) {
30523052
}
30533053
val (formalCaptures, actualCaptures) = formalAndActualCaptures.unzip
30543054

3055+
val funInterfaceSym = functionalInterface.tpe.typeSymbol
3056+
val hasRepeatedParam = {
3057+
funInterfaceSym.exists && {
3058+
val Seq(samMethodDenot) = funInterfaceSym.info.possibleSamMethods
3059+
val samMethod = samMethodDenot.symbol
3060+
atPhase(elimRepeatedPhase)(samMethod.info.paramInfoss.flatten.exists(_.isRepeatedParam))
3061+
}
3062+
}
3063+
30553064
val formalParamNames = sym.info.paramNamess.flatten.drop(envSize)
30563065
val formalParamTypes = sym.info.paramInfoss.flatten.drop(envSize)
3057-
val formalParamNamesAndTypes = formalParamNames.zip(formalParamTypes)
3058-
val formalAndActualParams = formalParamNamesAndTypes.map {
3059-
case (name, tpe) =>
3066+
val formalParamRepeateds =
3067+
if (hasRepeatedParam) (0 until (formalParamTypes.size - 1)).map(_ => false) :+ true
3068+
else (0 until formalParamTypes.size).map(_ => false)
3069+
3070+
val formalAndActualParams = formalParamNames.lazyZip(formalParamTypes).lazyZip(formalParamRepeateds).map {
3071+
(name, tpe, repeated) =>
30603072
val formalParam = js.ParamDef(freshLocalIdent(name.toString),
30613073
OriginalName(name.toString), jstpe.AnyType, mutable = false)
3062-
val actualParam = unbox(formalParam.ref, tpe)
3074+
val actualParam =
3075+
if (repeated) genJSArrayToVarArgs(formalParam.ref)(tree.sourcePos)
3076+
else unbox(formalParam.ref, tpe)
30633077
(formalParam, actualParam)
30643078
}
3065-
val (formalParams, actualParams) = formalAndActualParams.unzip
3079+
val (formalAndRestParams, actualParams) = formalAndActualParams.unzip
3080+
3081+
val (formalParams, restParam) =
3082+
if (hasRepeatedParam) (formalAndRestParams.init, Some(formalAndRestParams.last))
3083+
else (formalAndRestParams, None)
30663084

30673085
val genBody = {
30683086
val call = if (isStaticCall) {
@@ -3077,34 +3095,41 @@ class JSCodeGen()(using genCtx: Context) {
30773095
box(call, sym.info.finalResultType)
30783096
}
30793097

3080-
val funInterfaceSym = functionalInterface.tpe.typeSymbol
3098+
val isThisFunction = funInterfaceSym.isSubClass(jsdefn.JSThisFunctionClass) && {
3099+
val ok = formalParams.nonEmpty
3100+
if (!ok)
3101+
report.error("The SAM or apply method for a js.ThisFunction must have a leading non-varargs parameter", tree)
3102+
ok
3103+
}
30813104

3082-
if (jsdefn.isJSThisFunctionClass(funInterfaceSym)) {
3105+
if (isThisFunction) {
30833106
val thisParam :: otherParams = formalParams
30843107
js.Closure(
30853108
arrow = false,
30863109
formalCaptures,
30873110
otherParams,
3088-
None,
3111+
restParam,
30893112
js.Block(
30903113
js.VarDef(thisParam.name, thisParam.originalName,
30913114
thisParam.ptpe, mutable = false,
30923115
js.This()(thisParam.ptpe)(thisParam.pos))(thisParam.pos),
30933116
genBody),
30943117
actualCaptures)
30953118
} else {
3096-
val closure = js.Closure(arrow = true, formalCaptures, formalParams, None, genBody, actualCaptures)
3119+
val closure = js.Closure(arrow = true, formalCaptures, formalParams, restParam, genBody, actualCaptures)
30973120

3098-
if (jsdefn.isJSFunctionClass(funInterfaceSym)) {
3099-
closure
3100-
} else {
3121+
if (!funInterfaceSym.exists || defn.isFunctionClass(funInterfaceSym)) {
31013122
assert(!funInterfaceSym.exists || defn.isFunctionClass(funInterfaceSym),
31023123
s"Invalid functional interface $funInterfaceSym reached the back-end")
31033124
val formalCount = formalParams.size
31043125
val cls = ClassName("scala.scalajs.runtime.AnonFunction" + formalCount)
31053126
val ctorName = MethodName.constructor(
31063127
jstpe.ClassRef(ClassName("scala.scalajs.js.Function" + formalCount)) :: Nil)
31073128
js.New(cls, js.MethodIdent(ctorName), List(closure))
3129+
} else {
3130+
assert(funInterfaceSym.isJSType,
3131+
s"Invalid functional interface $funInterfaceSym reached the back-end")
3132+
closure
31083133
}
31093134
}
31103135
}

compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ final class JSDefinitions()(using Context) {
4646
def JSAnyClass(using Context) = JSAnyType.symbol.asClass
4747
@threadUnsafe lazy val JSObjectType: TypeRef = requiredClassRef("scala.scalajs.js.Object")
4848
def JSObjectClass(using Context) = JSObjectType.symbol.asClass
49-
@threadUnsafe lazy val JSBaseThisFunctionType: TypeRef = requiredClassRef("scala.scalajs.js.ThisFunction")
50-
def JSBaseThisFunctionClass(using Context) = JSBaseThisFunctionType.symbol.asClass
49+
@threadUnsafe lazy val JSFunctionType: TypeRef = requiredClassRef("scala.scalajs.js.Function")
50+
def JSFunctionClass(using Context) = JSFunctionType.symbol.asClass
51+
@threadUnsafe lazy val JSThisFunctionType: TypeRef = requiredClassRef("scala.scalajs.js.ThisFunction")
52+
def JSThisFunctionClass(using Context) = JSThisFunctionType.symbol.asClass
5153

5254
@threadUnsafe lazy val PseudoUnionType: TypeRef = requiredClassRef("scala.scalajs.js.|")
5355
def PseudoUnionClass(using Context) = PseudoUnionType.symbol.asClass
@@ -66,11 +68,6 @@ final class JSDefinitions()(using Context) {
6668
@threadUnsafe lazy val JSDynamicType: TypeRef = requiredClassRef("scala.scalajs.js.Dynamic")
6769
def JSDynamicClass(using Context) = JSDynamicType.symbol.asClass
6870

69-
@threadUnsafe lazy val JSFunctionType = (0 to 22).map(n => requiredClassRef("scala.scalajs.js.Function" + n)).toArray
70-
def JSFunctionClass(n: Int)(using Context) = JSFunctionType(n).symbol.asClass
71-
@threadUnsafe lazy val JSThisFunctionType = (0 to 21).map(n => requiredClassRef("scala.scalajs.js.ThisFunction" + n)).toArray
72-
def JSThisFunctionClass(n: Int)(using Context) = JSThisFunctionType(n).symbol.asClass
73-
7471
@threadUnsafe lazy val RuntimeExceptionType: TypeRef = requiredClassRef("java.lang.RuntimeException")
7572
def RuntimeExceptionClass(using Context) = RuntimeExceptionType.symbol.asClass
7673
@threadUnsafe lazy val JavaScriptExceptionType: TypeRef = requiredClassRef("scala.scalajs.js.JavaScriptException")
@@ -236,27 +233,6 @@ final class JSDefinitions()(using Context) {
236233
allRefClassesCache
237234
}
238235

239-
/** If `cls` is a class in the scala package, its name, otherwise EmptyTypeName */
240-
private def scalajsClassName(cls: Symbol)(using Context): TypeName =
241-
if (cls.isClass && cls.owner == ScalaJSJSPackageClass) cls.asClass.name
242-
else EmptyTypeName
243-
244-
/** Is the given `cls` a class of the form `scala.scalajs.js.prefixN` where
245-
* `N` is a number.
246-
*
247-
* This is similar to `isVarArityClass` in `Definitions.scala`.
248-
*/
249-
private def isScalaJSVarArityClass(cls: Symbol, prefix: String): Boolean = {
250-
val name = scalajsClassName(cls)
251-
name.startsWith(prefix) && name.toString.drop(prefix.length).forall(_.isDigit)
252-
}
253-
254-
def isJSFunctionClass(cls: Symbol): Boolean =
255-
isScalaJSVarArityClass(cls, str.Function)
256-
257-
def isJSThisFunctionClass(cls: Symbol): Boolean =
258-
isScalaJSVarArityClass(cls, "ThisFunction")
259-
260236
/** Definitions related to the treatment of JUnit bootstrappers. */
261237
object junit {
262238
@threadUnsafe lazy val TestAnnotType: TypeRef = requiredClassRef("org.junit.Test")

compiler/src/dotty/tools/dotc/config/SJSPlatform.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ class SJSPlatform()(using Context) extends JavaPlatform {
2323
/** Is the SAMType `cls` also a SAM under the rules of the Scala.js back-end? */
2424
override def isSam(cls: ClassSymbol)(using Context): Boolean =
2525
defn.isFunctionClass(cls)
26-
|| jsDefinitions.isJSFunctionClass(cls)
27-
|| jsDefinitions.isJSThisFunctionClass(cls)
26+
|| cls.superClass == jsDefinitions.JSFunctionClass
2827

2928
/** Is the given class symbol eligible for Java serialization-specific methods?
3029
*

compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ object JSSymUtils {
161161
def jsCallingConvention(using Context): JSCallingConvention =
162162
JSCallingConvention.of(sym)
163163

164+
def hasJSCallCallingConvention(using Context): Boolean =
165+
sym.jsCallingConvention == JSCallingConvention.Call
166+
164167
/** Gets the unqualified JS name of the symbol.
165168
*
166169
* If it is not explicitly specified with an `@JSName` annotation, the

compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,32 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
241241

242242
private def transformStatOrExpr(tree: Tree)(using Context): Tree = {
243243
tree match {
244+
case Closure(env, call, functionalInterface) =>
245+
val tpeSym = functionalInterface.tpe.typeSymbol
246+
if (tpeSym.isJSType) {
247+
def reportError(reasonAndExplanation: String): Unit = {
248+
report.error(
249+
"Using an anonymous function as a SAM for the JavaScript type " +
250+
i"${tpeSym.fullName} is not allowed because " +
251+
reasonAndExplanation,
252+
tree)
253+
}
254+
if (!tpeSym.is(Trait) || tpeSym.asClass.superClass != jsdefn.JSFunctionClass) {
255+
reportError(
256+
"it is not a trait extending js.Function. " +
257+
"Use an anonymous class instead.")
258+
} else if (tpeSym.hasAnnotation(jsdefn.JSNativeAnnot)) {
259+
reportError(
260+
"it is a native JS type. " +
261+
"It is not possible to directly implement it.")
262+
} else if (!tree.tpe.possibleSamMethods.exists(_.symbol.hasJSCallCallingConvention)) {
263+
reportError(
264+
"its single abstract method is not named `apply`. " +
265+
"Use an anonymous class instead.")
266+
}
267+
}
268+
super.transform(tree)
269+
244270
// Validate js.constructorOf[T]
245271
case TypeApply(ctorOfTree, List(tpeArg))
246272
if ctorOfTree.symbol == jsdefn.JSPackage_constructorOf =>
@@ -617,8 +643,27 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
617643
case JSCallingConvention.Property(_) => // checked above
618644
case JSCallingConvention.Method(_) => // no checks needed
619645

620-
case JSCallingConvention.Call =>
621-
report.error("A non-native JS class cannot declare a method named `apply` without `@JSName`", tree)
646+
case JSCallingConvention.Call if !sym.is(Deferred) =>
647+
report.error("A non-native JS class cannot declare a concrete method named `apply` without `@JSName`", tree)
648+
649+
case JSCallingConvention.Call => // if sym.isDeferred
650+
/* Allow an abstract `def apply` only if the owner is a plausible
651+
* JS function SAM trait.
652+
*/
653+
val owner = sym.owner
654+
val isPlausibleJSFunctionType = {
655+
owner.is(Trait) &&
656+
owner.asClass.superClass == jsdefn.JSFunctionClass &&
657+
owner.typeRef.possibleSamMethods.map(_.symbol) == Seq(sym) &&
658+
!sym.info.isInstanceOf[PolyType]
659+
}
660+
if (!isPlausibleJSFunctionType) {
661+
report.error(
662+
"A non-native JS type can only declare an abstract method named `apply` without `@JSName` " +
663+
"if it is the SAM of a trait that extends js.Function",
664+
tree)
665+
}
666+
622667
case JSCallingConvention.BracketAccess =>
623668
report.error("@JSBracketAccess is not allowed in non-native JS classes", tree)
624669
case JSCallingConvention.BracketCall =>

project/Build.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,9 +1115,7 @@ object Build {
11151115
++ (dir / "js/src/test/require-2.12" ** (("*.scala": FileFilter)
11161116
-- "JSOptionalTest212.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in
11171117
)).get
1118-
++ (dir / "js/src/test/require-sam" ** (("*.scala": FileFilter)
1119-
-- "CustomJSFunctionTest.scala" // TODO: Custom JS function types are not implemented yet
1120-
)).get
1118+
++ (dir / "js/src/test/require-sam" ** "*.scala").get
11211119
++ (dir / "js/src/test/scala-new-collections" ** "*.scala").get
11221120
)
11231121
},
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import scala.scalajs.js
2+
import scala.scalajs.js.annotation.*
3+
4+
trait BadThisFunction1 extends js.ThisFunction {
5+
def apply(): Int
6+
}
7+
trait BadThisFunction2 extends js.ThisFunction {
8+
def apply(args: Int*): Int
9+
}
10+
class A {
11+
val badThisFunction1: BadThisFunction1 = () => 42 // error
12+
val badThisFunction2: BadThisFunction2 = args => args.size // error
13+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import scala.scalajs.js
2+
import scala.scalajs.js.annotation.*
3+
4+
class BadFunctionIsClass extends js.Function {
5+
def apply(x: Int): Int // error
6+
}
7+
trait BadFunctionExtendsNonFunction extends js.Object {
8+
def apply(x: Int): Int // error
9+
}
10+
class SubclassOfFunction extends js.Function
11+
trait BadFunctionExtendsSubclassOfFunction extends SubclassOfFunction {
12+
def apply(x: Int): Int // error
13+
}
14+
trait BadFunctionParametricMethod extends js.Function {
15+
def apply[A](x: A): A // error
16+
}
17+
trait BadFunctionOverloaded extends js.Function {
18+
def apply(x: Int): Int // error
19+
def apply(x: String): String // error
20+
}
21+
trait BadFunctionMultipleAbstract extends js.Function {
22+
def apply(x: Int): Int // error
23+
def foo(x: Int): Int
24+
}

tests/neg-scalajs/js-non-native-members.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- Error: tests/neg-scalajs/js-non-native-members.scala:5:6 ------------------------------------------------------------
22
5 | def apply(arg: Int): Int = arg // error
33
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4-
| A non-native JS class cannot declare a method named `apply` without `@JSName`
4+
| A non-native JS class cannot declare a concrete method named `apply` without `@JSName`
55
-- Error: tests/neg-scalajs/js-non-native-members.scala:8:6 ------------------------------------------------------------
66
7 | @JSBracketAccess
77
8 | def foo(index: Int, arg: Int): Int = arg // error
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import scala.scalajs.js
2+
import scala.scalajs.js.annotation.*
3+
4+
@js.native
5+
trait A1 extends js.Object {
6+
def foo(x: Int): Int
7+
}
8+
trait A2 extends js.Object {
9+
def bar(x: Int): Int
10+
}
11+
class A3 extends js.Function {
12+
def foobar(x: Int): Int
13+
}
14+
class A4 {
15+
val foo: A1 = x => x + 1 // error
16+
val bar: A2 = x => x + 1 // error
17+
val foobar: A3 = x => x + 1 // error
18+
}
19+
20+
@js.native
21+
trait B1 extends js.Function {
22+
def apply(x: Int): Int
23+
}
24+
@js.native
25+
trait B2 extends js.Function {
26+
def bar(x: Int = 5): Int
27+
}
28+
class B3 {
29+
val foo: B1 = x => x + 1 // error
30+
val bar: B2 = x => x + 1 // error
31+
}
32+
33+
trait C1 extends js.Function {
34+
def foo(x: Int): Int
35+
}
36+
class C2 {
37+
val foo: C1 = x => x + 1 // error
38+
}

0 commit comments

Comments
 (0)