Skip to content

Commit 204acae

Browse files
committed
Emit lifted methods and lambda methods as static if possible.
For all lifted methods and methods that contain the body of lambdas, we analyze whether they (transitively) refer to `this`. If not, we emit them as static. The JVM backend does a similar analysis in the `delambdafy` phase, which comes after our backend. As a side effect, this solves an issue with lifted local methods coming from arguments to a super constructor call. They would previously result in references to `this` from the super constructor call arguments, which we will not consider as valid IR anymore. Since Scala lexical scoping rules ensure that they do not actually refer to `this`, they will now always be compiled as static.
1 parent 0bc8c06 commit 204acae

File tree

1 file changed

+139
-13
lines changed

1 file changed

+139
-13
lines changed

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

Lines changed: 139 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,138 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
248248
)(withNewLocalNameScope(body))
249249
}
250250

251+
// Global state for tracking methods that reference `this` -----------------
252+
253+
/* scalac generates private instance methods for
254+
* a) local defs and
255+
* b) anonymous functions.
256+
*
257+
* In many cases, these methods do not need access to the enclosing `this`
258+
* value. For every other local variable, `lambdalift` only adds them as
259+
* arguments when they are needed; but `this` is always implicitly added
260+
* because all such methods are instance methods.
261+
*
262+
* We run a separate analysis to figure out which of those methods actually
263+
* need their `this` value. We compile those that do not as static methods,
264+
* as if they were `isStaticMember`.
265+
*
266+
* The `delambdafy` phase of scalac performs a similar analysis, although
267+
* as it runs after our backend (for other reasons), we do not benefit from
268+
* it. Our analysis is much simpler because it deals with local defs and
269+
* anonymous functions in a unified way.
270+
*
271+
* Performing this analysis is particularly important for lifted methods
272+
* that appear within arguments to a super constructor call. The lexical
273+
* scope of Scala guarantees that they cannot use `this`, but if they are
274+
* compiled as instance methods, they force the constructor to leak the
275+
* `this` value before the super constructor call, which is invalid.
276+
* While most of the time this analysis is only an optimization, in those
277+
* (rare) situations it is required for correctness. See for example
278+
* the partest `run/t8733.scala`.
279+
*/
280+
281+
private var statifyCandidateMethodsThatReferenceThis: scala.collection.Set[Symbol] = null
282+
283+
/** Is that given method a statify-candidate?
284+
*
285+
* If a method is a statify-candidate, we will analyze whether it actually
286+
* needs its `this` value. If it does not, we will compile it as a static
287+
* method in the IR.
288+
*
289+
* A method is a statify-candidate if it is a lifted method (a local def)
290+
* or the target of an anonymous function.
291+
*
292+
* TODO Currently we also require that the method owner not be a JS type.
293+
* We should relax that restriction in the future.
294+
*/
295+
private def isStatifyCandidate(sym: Symbol): Boolean =
296+
(sym.isLiftedMethod || sym.isDelambdafyTarget) && !isJSType(sym.owner)
297+
298+
/** Do we compile the given method as a static method in the IR?
299+
*
300+
* This is true if one of the following is true:
301+
*
302+
* - It is `isStaticMember`, or
303+
* - It is a statify-candidate method and it does not reference `this`.
304+
*/
305+
private def compileAsStaticMethod(sym: Symbol): Boolean = {
306+
sym.isStaticMember || {
307+
isStatifyCandidate(sym) &&
308+
!statifyCandidateMethodsThatReferenceThis.contains(sym)
309+
}
310+
}
311+
312+
/** Finds all statify-candidate methods that reference `this`, inspired by Delambdafy. */
313+
private final class ThisReferringMethodsTraverser extends Traverser {
314+
// the set of statify-candidate methods that directly refer to `this`
315+
private val roots = mutable.Set.empty[Symbol]
316+
317+
// for each statify-candidate method `m`, the set of statify-candidate methods that call `m`
318+
private val methodReferences = mutable.Map.empty[Symbol, mutable.Set[Symbol]]
319+
320+
def methodReferencesThisIn(tree: Tree): collection.Set[Symbol] = {
321+
traverse(tree)
322+
propagateReferences()
323+
}
324+
325+
private def addRoot(symbol: Symbol): Unit =
326+
roots += symbol
327+
328+
private def addReference(from: Symbol, to: Symbol): Unit =
329+
methodReferences.getOrElseUpdate(to, mutable.Set.empty) += from
330+
331+
private def propagateReferences(): collection.Set[Symbol] = {
332+
val result = mutable.Set.empty[Symbol]
333+
334+
def rec(symbol: Symbol): Unit = {
335+
// mark `symbol` as referring to `this`
336+
if (result.add(symbol)) {
337+
// `symbol` was not yet in the set; propagate further
338+
for {
339+
referrers <- methodReferences.get(symbol)
340+
referrer <- referrers
341+
} {
342+
// referrer calls `symbol`, so it transitively references `this`
343+
rec(referrer)
344+
}
345+
}
346+
}
347+
348+
roots.foreach(rec(_))
349+
350+
result
351+
}
352+
353+
private var currentMethod: Symbol = NoSymbol
354+
355+
override def traverse(tree: Tree): Unit = tree match {
356+
case _: DefDef =>
357+
if (isStatifyCandidate(tree.symbol)) {
358+
currentMethod = tree.symbol
359+
super.traverse(tree)
360+
currentMethod = NoSymbol
361+
} else {
362+
// No need to traverse other methods; we always assume they refer to `this`.
363+
}
364+
case Function(_, Apply(target, _)) =>
365+
/* We don't drill into functions because at this phase they will always refer to `this`.
366+
* They will be of the form {(args...) => this.anonfun(args...)}
367+
* but we do need to make note of the lifted body method in case it refers to `this`.
368+
*/
369+
if (currentMethod.exists)
370+
addReference(from = currentMethod, to = target.symbol)
371+
case Apply(sel @ Select(This(_), _), args) if isStatifyCandidate(sel.symbol) =>
372+
if (currentMethod.exists)
373+
addReference(from = currentMethod, to = sel.symbol)
374+
super.traverseTrees(args)
375+
case This(_) =>
376+
if (currentMethod.exists && tree.symbol == currentMethod.enclClass)
377+
addRoot(currentMethod)
378+
case _ =>
379+
super.traverse(tree)
380+
}
381+
}
382+
251383
// Global class generation state -------------------------------------------
252384

253385
private val lazilyGeneratedAnonClasses = mutable.Map.empty[Symbol, ClassDef]
@@ -306,6 +438,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
306438
*/
307439
override def apply(cunit: CompilationUnit): Unit = {
308440
try {
441+
statifyCandidateMethodsThatReferenceThis =
442+
new ThisReferringMethodsTraverser().methodReferencesThisIn(cunit.body)
443+
309444
def collectClassDefs(tree: Tree): List[ClassDef] = {
310445
tree match {
311446
case EmptyTree => Nil
@@ -455,6 +590,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
455590
lazilyGeneratedAnonClasses.clear()
456591
generatedStaticForwarderClasses.clear()
457592
generatedClasses.clear()
593+
statifyCandidateMethodsThatReferenceThis = null
458594
pos2irPosCache.clear()
459595
}
460596
}
@@ -1144,16 +1280,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
11441280

11451281
assert(moduleClass.isModuleClass, moduleClass)
11461282

1147-
val hasAnyExistingPublicStaticMethod =
1148-
existingMethods.exists(_.flags.namespace == js.MemberNamespace.PublicStatic)
1149-
if (hasAnyExistingPublicStaticMethod) {
1150-
reporter.error(pos,
1151-
"Unexpected situation: found existing public static methods in " +
1152-
s"the class ${moduleClass.fullName} while trying to generate " +
1153-
"static forwarders for its companion object. " +
1154-
"Please report this as a bug in Scala.js.")
1155-
}
1156-
11571283
def listMembersBasedOnFlags = {
11581284
// Copy-pasted from BCodeHelpers.
11591285
val ExcludedForwarderFlags: Long = {
@@ -1964,7 +2090,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
19642090
} else {
19652091
val resultIRType = toIRType(sym.tpe.resultType)
19662092
val namespace = {
1967-
if (sym.isStaticMember) {
2093+
if (compileAsStaticMethod(sym)) {
19682094
if (sym.isPrivate) js.MemberNamespace.PrivateStatic
19692095
else js.MemberNamespace.PublicStatic
19702096
} else {
@@ -3453,7 +3579,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
34533579
genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args))
34543580
} else if (sym.hasAnnotation(JSNativeAnnotation)) {
34553581
genJSNativeMemberCall(tree, isStat)
3456-
} else if (sym.isStaticMember) {
3582+
} else if (compileAsStaticMethod(sym)) {
34573583
if (sym.isMixinConstructor) {
34583584
/* Do not emit a call to the $init$ method of JS traits.
34593585
* This exception is necessary because optional JS fields cause the
@@ -6333,7 +6459,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
63336459

63346460
val formalArgs = params.map(genParamDef(_))
63356461

6336-
val (allFormalCaptures, body, allActualCaptures) = if (!target.isStaticMember) {
6462+
val (allFormalCaptures, body, allActualCaptures) = if (!compileAsStaticMethod(target)) {
63376463
val thisActualCapture = genExpr(receiver)
63386464
val thisFormalCapture = js.ParamDef(
63396465
freshLocalIdent("this")(receiver.pos), thisOriginalName,

0 commit comments

Comments
 (0)