Skip to content

Commit 7edacf5

Browse files
committed
Fix #4372: Generalize js.Function's to allow user-defined ones.
Previously, there was a fixed set of built-in JS function types, `js.FunctionN` and `js.ThisFunctionN`, that represented JS function values. The only way to create JS function values was to have a lambda expression used as a SAM of these types. In this commit, we generalize these to any SAM-able non-native JS trait whose single abstract method is named `apply` and whose super-*class* is `js.Function`. This allows the user to define custom JS function types for signatures that are not made available by the built-in types. A typical example is signatures with rest parameters (i.e., varargs). To do this, we make the following changes: * We allow declaring an *abstract* `apply` method in a non-native JS trait if that makes that trait a SAM type with that method and if its superclass is `js.Function`. Even though we cannot in general implement `apply` methods, it is only the *concrete* ones that are problematic. * We apply the JS `Closure` treatment in the back-end to any anonymous function class that is the result of desugaring a lambda for such a SAM type. * In that treatment, we make a `function` function (as opposed to an arrow function) and use a `this` parameter if the anonymous function class inherits from js.ThisFunction. * We add logic to handle repeated parameters in the treatment as well. In addition, we remove the `@js.native` annotation from all the "built-in" `js.FunctionN` and `js.ThisFunctionN` types. They lose their prilileged status in the spec and in the compiler, to become regular definitions that fit the above spec.
1 parent 5f5414c commit 7edacf5

File tree

15 files changed

+556
-139
lines changed

15 files changed

+556
-139
lines changed

compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ trait CompatComponent {
6464
def isImplClass(sym: Symbol): Boolean =
6565
scalaUsesImplClasses && sym.hasFlag(Flags.IMPLCLASS)
6666

67+
lazy val isScala211: Boolean = scalaUsesImplClasses
68+
6769
implicit final class StdTermNamesCompat(self: global.nme.type) {
6870
def IMPL_CLASS_SUFFIX: String = noImplClasses()
6971

compiler/src/main/scala/org/scalajs/nscplugin/ExplicitLocalJS.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ abstract class ExplicitLocalJS[G <: Global with Singleton](val global: G)
127127

128128
import global._
129129
import jsAddons._
130-
import jsInterop.jsclassAccessorFor
130+
import jsInterop.{jsclassAccessorFor, JSCallingConvention}
131131
import definitions._
132132
import rootMirror._
133133
import jsDefinitions._
@@ -171,8 +171,12 @@ abstract class ExplicitLocalJS[G <: Global with Singleton](val global: G)
171171

172172
/** Is the given clazz a local JS class or object? */
173173
private def isLocalJSClassOrObject(clazz: Symbol): Boolean = {
174-
def isJSLambda =
175-
clazz.isAnonymousClass && AllJSFunctionClasses.exists(clazz.isSubClass(_))
174+
def isJSLambda: Boolean = {
175+
// See GenJSCode.isJSFunctionDef
176+
clazz.isAnonymousClass &&
177+
clazz.superClass == JSFunctionClass &&
178+
clazz.info.decl(nme.apply).filter(JSCallingConvention.isCall(_)).exists
179+
}
176180

177181
clazz.isLocalToBlock &&
178182
!clazz.isTrait && clazz.hasAnnotation(JSTypeAnnot) &&

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

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5796,6 +5796,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
57965796
if (name == "apply" || (ddsym.isSpecialized && name.startsWith("apply$"))) {
57975797
if ((applyDef eq null) || ddsym.isSpecialized)
57985798
applyDef = dd
5799+
} else if (ddsym.hasAnnotation(JSOptionalAnnotation)) {
5800+
// Ignore (this is useful for default parameters in custom JS function types)
57995801
} else {
58005802
// Found a method we cannot encode in the rewriting
58015803
fail(s"Found a non-apply method $ddsym in $cd")
@@ -5847,15 +5849,31 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
58475849

58485850
// Fourth step: patch the body to unbox parameters and box result
58495851

5852+
val hasRepeatedParam = {
5853+
sym.superClass == JSFunctionClass && // Scala functions are known not to have repeated params
5854+
enteringUncurry {
5855+
applyDef.symbol.paramss.flatten.lastOption.exists(isRepeated(_))
5856+
}
5857+
}
5858+
58505859
val js.MethodDef(_, _, _, params, _, body) = applyMethod
5851-
val (patchedParams, patchedBody) =
5852-
patchFunBodyWithBoxes(applyDef.symbol, params, body.get)
5860+
val (patchedParams, patchedBody) = {
5861+
patchFunBodyWithBoxes(applyDef.symbol, params, body.get,
5862+
useParamsBeforeLambdaLift = false,
5863+
hasRepeatedParam = hasRepeatedParam)
5864+
}
58535865

58545866
// Fifth step: build the js.Closure
58555867

5856-
val isThisFunction = JSThisFunctionClasses.exists(sym isSubClass _)
5857-
assert(!isThisFunction || patchedParams.nonEmpty,
5858-
s"Empty param list in ThisFunction: $cd")
5868+
val isThisFunction = sym.isSubClass(JSThisFunctionClass) && {
5869+
val ok = patchedParams.headOption.exists(!_.rest)
5870+
if (!ok) {
5871+
reporter.error(pos,
5872+
"The SAM or apply method for a js.ThisFunction must have a " +
5873+
"leading non-varargs parameter")
5874+
}
5875+
ok
5876+
}
58595877

58605878
val capturedArgs =
58615879
if (hasUnusedOuterCtorParam) initialCapturedArgs.tail
@@ -5973,7 +5991,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
59735991

59745992
val (patchedFormalArgs, patchedBody) = {
59755993
patchFunBodyWithBoxes(target, formalArgs, body,
5976-
useParamsBeforeLambdaLift = true)
5994+
useParamsBeforeLambdaLift = true, hasRepeatedParam = false)
59775995
}
59785996

59795997
val closure = js.Closure(
@@ -6121,7 +6139,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
61216139

61226140
private def patchFunBodyWithBoxes(methodSym: Symbol,
61236141
params: List[js.ParamDef], body: js.Tree,
6124-
useParamsBeforeLambdaLift: Boolean = false)(
6142+
useParamsBeforeLambdaLift: Boolean, hasRepeatedParam: Boolean)(
61256143
implicit pos: Position): (List[js.ParamDef], js.Tree) = {
61266144
val methodType = enteringPhase(currentRun.posterasurePhase)(methodSym.tpe)
61276145

@@ -6147,18 +6165,26 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
61476165
methodSym.tpe.params
61486166
}
61496167

6168+
val theRepeatedParamOrNull =
6169+
if (!hasRepeatedParam) null
6170+
else params.last
6171+
61506172
val (patchedParams, paramsLocal) = (for {
61516173
(param, paramSym) <- params zip paramSyms
61526174
} yield {
6153-
val paramTpe = paramTpes.getOrElse(paramSym.name, paramSym.tpe)
6175+
val isRepeated = param eq theRepeatedParamOrNull
6176+
def paramTpe = paramTpes.getOrElse(paramSym.name, paramSym.tpe)
61546177
val paramNameIdent = param.name
61556178
val origName = param.originalName
61566179
val newNameIdent = freshLocalIdent(paramNameIdent.name)(paramNameIdent.pos)
61576180
val newOrigName = origName.orElse(paramNameIdent.name)
61586181
val patchedParam = js.ParamDef(newNameIdent, newOrigName, jstpe.AnyType,
6159-
mutable = false, rest = param.rest)(param.pos)
6182+
mutable = false, rest = isRepeated)(param.pos)
6183+
val paramLocalRhs =
6184+
if (isRepeated) genJSArrayToVarArgs(patchedParam.ref)
6185+
else fromAny(patchedParam.ref, paramTpe)
61606186
val paramLocal = js.VarDef(paramNameIdent, origName, param.ptpe,
6161-
mutable = false, fromAny(patchedParam.ref, paramTpe))
6187+
mutable = false, paramLocalRhs)
61626188
(patchedParam, paramLocal)
61636189
}).unzip
61646190

@@ -6532,16 +6558,16 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
65326558
* non-lambda, concrete, non-native JS type, cannot implement a method named
65336559
* `apply`.
65346560
*
6535-
* All JS function classes have an abstract member named `apply`. Therefore,
6536-
* a class is a JS lambda iff it is concrete, a non-native JS type and
6537-
* inherits from a JS function class.
6561+
* Therefore, a class is a JS lambda iff it is anonymous, its direct
6562+
* super class is `js.Function`, and it contains an implementation of an
6563+
* `apply` method.
65386564
*
6539-
* To avoid having to an isSubClass check on all concrete non-native JS
6540-
* classes, we short-circuit check that the class is an anonymous class
6541-
* (a necessary, but not sufficient condition for a JS lambda).
6565+
* Note that being anonymous implies being concrete and non-native, so we
6566+
* do not have to test that.
65426567
*/
6543-
!isJSNativeClass(sym) && !sym.isAbstract && sym.isAnonymousClass &&
6544-
AllJSFunctionClasses.exists(sym.isSubClass(_))
6568+
sym.isAnonymousClass &&
6569+
sym.superClass == JSFunctionClass &&
6570+
sym.info.decl(nme.apply).filter(JSCallingConvention.isCall(_)).exists
65456571
}
65466572

65476573
private def isJSCtorDefaultParam(sym: Symbol) = {

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,13 @@ trait JSDefinitions {
5151
lazy val JSAnyClass = getRequiredClass("scala.scalajs.js.Any")
5252
lazy val JSDynamicClass = getRequiredClass("scala.scalajs.js.Dynamic")
5353
lazy val JSObjectClass = getRequiredClass("scala.scalajs.js.Object")
54+
lazy val JSFunctionClass = getRequiredClass("scala.scalajs.js.Function")
5455
lazy val JSThisFunctionClass = getRequiredClass("scala.scalajs.js.ThisFunction")
5556

5657
lazy val UnionClass = getRequiredClass("scala.scalajs.js.$bar")
5758

5859
lazy val JSArrayClass = getRequiredClass("scala.scalajs.js.Array")
5960

60-
lazy val JSFunctionClasses = (0 to 22) map (n => getRequiredClass("scala.scalajs.js.Function"+n))
61-
lazy val JSThisFunctionClasses = (0 to 21) map (n => getRequiredClass("scala.scalajs.js.ThisFunction"+n))
62-
lazy val AllJSFunctionClasses = JSFunctionClasses ++ JSThisFunctionClasses
63-
6461
lazy val JavaScriptExceptionClass = getClassIfDefined("scala.scalajs.js.JavaScriptException")
6562

6663
lazy val JSNameAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSName")

compiler/src/main/scala/org/scalajs/nscplugin/JSGlobalAddons.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ trait JSGlobalAddons extends JSDefinitions
199199
}
200200
}
201201
}
202+
203+
/** Tests whether the calling convention of the specified symbol is `Call`.
204+
*
205+
* This helper is provided because we use this test in a few places.
206+
*/
207+
def isCall(sym: Symbol): Boolean =
208+
of(sym) == Call
202209
}
203210

204211
private object JSUnaryOpMethodName {

compiler/src/main/scala/org/scalajs/nscplugin/PrepJSInterop.scala

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -313,16 +313,32 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G)
313313
private def transformStatOrExpr(tree: Tree): Tree = {
314314
tree match {
315315
/* Anonymous function, need to check that it is not used as a SAM for a
316-
* JS type, unless it is js.FunctionN or js.ThisFunctionN.
316+
* JS type, unless it is a JS function type.
317317
* See #2921.
318318
*/
319319
case tree: Function =>
320+
// tpeSym is the type of the target SAM type (not the to-be-generated anonymous class)
320321
val tpeSym = tree.tpe.typeSymbol
321-
if (isJSAny(tpeSym) && !AllJSFunctionClasses.contains(tpeSym)) {
322-
reporter.error(tree.pos,
323-
"Using an anonymous function as a SAM for the JavaScript " +
324-
"type " + tpeSym.fullNameString + " is not allowed. " +
325-
"Use an anonymous class instead.")
322+
if (isJSAny(tpeSym)) {
323+
def reportError(reasonAndExplanation: String): Unit = {
324+
reporter.error(tree.pos,
325+
"Using an anonymous function as a SAM for the JavaScript " +
326+
s"type ${tpeSym.fullNameString} is not allowed because " +
327+
reasonAndExplanation)
328+
}
329+
if (!tpeSym.isTrait || tpeSym.superClass != JSFunctionClass) {
330+
reportError(
331+
"it is not a trait extending js.Function. " +
332+
"Use an anonymous class instead.")
333+
} else if (tpeSym.hasAnnotation(JSNativeAnnotation)) {
334+
reportError(
335+
"it is a native JS type. " +
336+
"It is not possible to directly implement it.")
337+
} else if (!JSCallingConvention.isCall(samOf(tree.tpe))) {
338+
reportError(
339+
"its single abstract method is not named `apply`. " +
340+
"Use an anonymous class instead.")
341+
}
326342
}
327343
super.transform(tree)
328344

@@ -550,48 +566,11 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G)
550566

551567
sym.addAnnotation(JSTypeAnnot)
552568

553-
val isJSLambda = {
554-
/* Under 2.11, sym.isAnonymousFunction does not properly recognize
555-
* anonymous functions here (because they seem to not be marked as
556-
* synthetic).
557-
*/
558-
sym.name == tpnme.ANON_FUN_NAME &&
559-
sym.info.member(nme.apply).isSynthetic &&
560-
AllJSFunctionClasses.exists(sym.isSubClass(_))
561-
}
562-
563-
if (isJSLambda)
564-
transformJSLambdaImplDef(implDef)
565-
else
566-
transformNonLambdaJSImplDef(implDef)
567-
}
568-
569-
/** Performs checks and rewrites specific to JS lambdas, i.e., anonymous
570-
* classes extending one of the JS function types.
571-
*
572-
* JS lambdas are special because they are completely hijacked by the
573-
* back-end, so although at this phase they look like normal anonymous
574-
* classes, they do not behave like ones.
575-
*/
576-
private def transformJSLambdaImplDef(implDef: ImplDef): Tree = {
577-
/* For the purposes of checking inner members, a JS lambda acts as a JS
578-
* native class owner.
579-
*
580-
* TODO This is probably not right, but historically it has always been
581-
* that way. It should be revisited.
582-
*/
583-
enterOwner(OwnerKind.JSNativeClass) {
584-
super.transform(implDef)
585-
}
586-
}
587-
588-
/** Performs checks and rewrites for all JS classes, traits and objects
589-
* except JS lambdas.
590-
*/
591-
private def transformNonLambdaJSImplDef(implDef: ImplDef): Tree = {
592-
val sym = moduleToModuleClass(implDef.symbol)
593569
val isJSNative = sym.hasAnnotation(JSNativeAnnotation)
594570

571+
val isJSFunctionSAMInScala211 =
572+
isScala211 && sym.name == tpnme.ANON_FUN_NAME && sym.superClass == JSFunctionClass
573+
595574
// Forbid @EnableReflectiveInstantiation on JS types
596575
sym.getAnnotation(EnableReflectiveInstantiationAnnotation).foreach {
597576
annot =>
@@ -634,6 +613,12 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G)
634613
* and similar constructs.
635614
* This causes the unsoundness filed as #1385.
636615
*/
616+
()
617+
case SerializableClass if isJSFunctionSAMInScala211 =>
618+
/* Ignore the scala.Serializable trait that Scala 2.11 adds on all
619+
* SAM classes when on a JS function SAM.
620+
*/
621+
()
637622
case parentSym =>
638623
/* This is a Scala class or trait other than AnyRef and Dynamic,
639624
* which is never valid.
@@ -644,6 +629,23 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G)
644629
}
645630
}
646631

632+
// Require that the SAM of a JS function def be `apply` (2.11-only here)
633+
if (isJSFunctionSAMInScala211) {
634+
if (!sym.info.decl(nme.apply).filter(JSCallingConvention.isCall(_)).exists) {
635+
val samType = sym.parentSymbols.find(_ != JSFunctionClass).getOrElse {
636+
/* This shouldn't happen, but fall back on this symbol (which has a
637+
* compiler-generated name) not to crash.
638+
*/
639+
sym
640+
}
641+
reporter.error(implDef.pos,
642+
"Using an anonymous function as a SAM for the JavaScript type " +
643+
s"${samType.fullNameString} is not allowed because its single " +
644+
"abstract method is not named `apply`. " +
645+
"Use an anonymous class instead.")
646+
}
647+
}
648+
647649
// Disallow bracket access / bracket call
648650
if (jsInterop.isJSBracketAccess(sym)) {
649651
reporter.error(implDef.pos,
@@ -982,10 +984,43 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G)
982984
case JSCallingConvention.Property(_) => // checked above
983985
case JSCallingConvention.Method(_) => // no checks needed
984986

985-
case JSCallingConvention.Call =>
986-
reporter.error(sym.pos,
987-
"A non-native JS class cannot declare a method " +
988-
"named `apply` without `@JSName`")
987+
case JSCallingConvention.Call if !sym.isDeferred =>
988+
/* Concrete `def apply` methods are normally rejected because we
989+
* cannot implement them in JavaScript. However, we do allow a
990+
* synthetic `apply` method if it is in a SAM for a JS function
991+
* type.
992+
*/
993+
val isJSFunctionSAM = {
994+
/* Under 2.11, sym.owner.isAnonymousFunction does not properly
995+
* recognize anonymous functions here (because they seem to not
996+
* be marked as synthetic).
997+
*/
998+
sym.isSynthetic &&
999+
sym.owner.name == tpnme.ANON_FUN_NAME &&
1000+
sym.owner.superClass == JSFunctionClass
1001+
}
1002+
if (!isJSFunctionSAM) {
1003+
reporter.error(sym.pos,
1004+
"A non-native JS class cannot declare a concrete method " +
1005+
"named `apply` without `@JSName`")
1006+
}
1007+
1008+
case JSCallingConvention.Call => // if sym.isDeferred
1009+
/* Allow an abstract `def apply` only if the owner is a plausible
1010+
* JS function SAM trait.
1011+
*/
1012+
val owner = sym.owner
1013+
val isPlausibleJSFunctionType = {
1014+
owner.isTrait &&
1015+
owner.superClass == JSFunctionClass &&
1016+
samOf(owner.toTypeConstructor) == sym
1017+
}
1018+
if (!isPlausibleJSFunctionType) {
1019+
reporter.error(sym.pos,
1020+
"A non-native JS type can only declare an abstract " +
1021+
"method named `apply` without `@JSName` if it is the " +
1022+
"SAM of a trait that extends js.Function")
1023+
}
9891024

9901025
case JSCallingConvention.BracketAccess =>
9911026
reporter.error(tree.pos,
@@ -1160,14 +1195,11 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G)
11601195
if (sym.isPrimaryConstructor || sym.isValueParameter ||
11611196
sym.isParamWithDefault || sym.isAccessor ||
11621197
sym.isParamAccessor || sym.isDeferred || sym.isSynthetic ||
1163-
AllJSFunctionClasses.contains(sym.owner) ||
11641198
(enclosingOwner is OwnerKind.JSNonNative)) {
11651199
/* Ignore (i.e. allow) primary ctor, parameters, default parameter
11661200
* getters, accessors, param accessors, abstract members, synthetic
11671201
* methods (to avoid double errors with case classes, e.g. generated
1168-
* copy method), js.Functions and js.ThisFunctions (they need abstract
1169-
* methods for SAM treatment), and any member of a non-native JS
1170-
* class/trait.
1202+
* copy method), and any member of a non-native JS class/trait.
11711203
*/
11721204
} else if (jsPrimitives.isJavaScriptPrimitive(sym)) {
11731205
// No check for primitives. We trust our own standard library.

compiler/src/test/scala/org/scalajs/nscplugin/test/JSOptionalTest.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ class JSOptionalTest extends DirectTest with TestHelpers {
8181
| def b(x: String = "foo"): Unit
8282
| ^
8383
"""
84+
85+
// Also for custom JS function types (2.11 has more errors than expected here)
86+
s"""
87+
trait A extends js.Function {
88+
def apply(x: js.UndefOr[Int] = 1): Int
89+
}
90+
""" containsErrors
91+
"""
92+
|newSource1.scala:6: error: Members of non-native JS traits may not have default parameters unless their default is `js.undefined`.
93+
| def apply(x: js.UndefOr[Int] = 1): Int
94+
| ^
95+
"""
8496
}
8597

8698
@Test

0 commit comments

Comments
 (0)