Skip to content

Commit caf77fc

Browse files
committed
Move the insertion of fake New nodes after erasure.
We enable Ycheck for our phases in the test suite to make sure that we do not regress in the future.
1 parent 379b690 commit caf77fc

File tree

5 files changed

+117
-30
lines changed

5 files changed

+117
-30
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class Compiler {
9999
new PureStats, // Remove pure stats from blocks
100100
new VCElideAllocations, // Peep-hole optimization to eliminate unnecessary value class allocations
101101
new ArrayApply, // Optimize `scala.Array.apply([....])` and `scala.Array.apply(..., [....])` into `[...]`
102+
new sjs.AddLocalJSFakeNews, // Adds fake new invocations to local JS classes in calls to `createLocalJSClass`
102103
new ElimPolyFunction, // Rewrite PolyFunction subclasses to FunctionN subclasses
103104
new TailRec, // Rewrite tail recursion to loops
104105
new CompleteJavaEnums, // Fill in constructors for Java enums

compiler/src/dotty/tools/dotc/transform/ExplicitOuter.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,13 @@ object ExplicitOuter {
384384
}
385385
else Nil
386386

387+
/** If the constructors of the given `cls` need to be passed an outer
388+
* argument, the singleton list with the argument, otherwise Nil.
389+
*/
390+
def argsForNew(cls: ClassSymbol, tpe: Type): List[Tree] =
391+
if (hasOuterParam(cls)) singleton(fixThis(outerPrefix(tpe))) :: Nil
392+
else Nil
393+
387394
/** A path of outer accessors starting from node `start`. `start` defaults to the
388395
* context owner's this node. There are two alternative conditions that determine
389396
* where the path ends:
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package dotty.tools
2+
package dotc
3+
package transform
4+
package sjs
5+
6+
import MegaPhase._
7+
import core.Constants
8+
import core.Contexts._
9+
import core.Decorators._
10+
import core.StdNames.nme
11+
import core.Phases._
12+
import core.Symbols._
13+
import core.Types._
14+
import ast.Trees._
15+
import dotty.tools.dotc.ast.tpd
16+
17+
import dotty.tools.backend.sjs.JSDefinitions.jsdefn
18+
19+
/** Adds fake calls to the constructors of local JS classes in calls to
20+
* `createLocalJSClass`.
21+
*
22+
* Given a call of the form
23+
* {{{
24+
* scala.scalajs.runtime.createLocalJSClass(classOf[C], jsClassValue, ???)
25+
* }}}
26+
* this phase fills in the `???` with an array of calls to the constructors
27+
* of `C`, like
28+
* {{{
29+
* [ new C(), new C(???, ???) : Object ]
30+
* }}}
31+
*
32+
* If the class `C` has an outer pointer, as determined by the `ExplicitOuter`
33+
* phase, the calls to the constructor insert a reference to the outer
34+
* instance:
35+
* {{{
36+
* [ new C(Enclosing.this), new C(Enclosing.this, ???, ???) : Object ]
37+
* }}}
38+
*
39+
* The `LambdaLift` phase will further expand those constructor calls with
40+
* values for captures. The back-end will extract the values of the outer
41+
* pointer and/or the captures to introduce them as JS class captures.
42+
*
43+
* Since we need to insert fake `new C()` calls, this scheme does not work for
44+
* abstract local classes. We therefore reject them as implementation
45+
* restriction in `PrepJSInterop`.
46+
*
47+
* This phase complements `ExplicitJSClasses`. The latter cannot create the
48+
* fake new invocations because that would require inventing sound type
49+
* arguments for the class' type parameters in order not to break Ycheck.
50+
*/
51+
class AddLocalJSFakeNews extends MiniPhase { thisPhase =>
52+
import ExplicitOuter.outer
53+
import ast.tpd._
54+
55+
override def phaseName: String = AddLocalJSFakeNews.name
56+
57+
override def isEnabled(using Context): Boolean =
58+
ctx.settings.scalajs.value
59+
60+
override def runsAfter: Set[String] = Set(Erasure.name)
61+
62+
override def transformApply(tree: Apply)(using Context): Tree = {
63+
if (tree.symbol == jsdefn.Runtime_createLocalJSClass) {
64+
val classValueArg :: superClassValueArg :: _ :: Nil = tree.args
65+
val cls = classValueArg match {
66+
case Literal(constant) if constant.tag == Constants.ClazzTag =>
67+
constant.typeValue.typeSymbol.asClass
68+
case _ =>
69+
// this shouldn't happen
70+
report.error(i"unexpected $classValueArg for the first argument to `createLocalJSClass`", classValueArg)
71+
jsdefn.JSObjectClass
72+
}
73+
74+
val fakeNews = {
75+
val ctors = cls.info.decls.lookupAll(nme.CONSTRUCTOR).toList.reverse
76+
val elems = ctors.map(ctor => fakeNew(cls, ctor.asTerm))
77+
JavaSeqLiteral(elems, TypeTree(defn.ObjectType))
78+
}
79+
80+
cpy.Apply(tree)(tree.fun, classValueArg :: superClassValueArg :: fakeNews :: Nil)
81+
} else {
82+
tree
83+
}
84+
}
85+
86+
/** Creates a fake invocation of the given class with the given constructor. */
87+
private def fakeNew(cls: ClassSymbol, ctor: TermSymbol)(using Context): Tree = {
88+
val tycon = cls.typeRef
89+
val outerArgs = outer.argsForNew(cls, tycon)
90+
val nonOuterArgCount = ctor.info.firstParamTypes.size - outerArgs.size
91+
val nonOuterArgs = List.fill(nonOuterArgCount)(ref(defn.Predef_undefined).appliedToNone)
92+
93+
New(tycon, ctor, outerArgs ::: nonOuterArgs)
94+
}
95+
}
96+
97+
object AddLocalJSFakeNews {
98+
val name: String = "addLocalJSFakeNews"
99+
}

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

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,15 @@ import JSSymUtils._
163163
* val Local\$jsclass: AnyRef = createLocalJSClass(
164164
* classOf[Local],
165165
* js.constructorOf[ParentJSClass],
166-
* Array[AnyRef](new Local(), ...))
166+
* ???)
167167
* }
168168
* }}}
169169
*
170-
* Since we need to insert fake `new Inner()`s, this scheme does not work for
171-
* abstract local classes. We therefore reject them as implementation
172-
* restriction in `PrepJSInterop`.
170+
* The third argument `???` is a placeholder, which will be filled in by
171+
* `AddLocalJSFakeNews` with fake new invocations for the all the constructors
172+
* of `Local`. We cannot do it at this phase because that would require
173+
* inventing sound type arguments for the type parameters of `Local` out of
174+
* thin air.
173175
*
174176
* If the body of `Local` references itself, then the `val Local\$jsclass` is
175177
* instead declared as a `var` to work around the cyclic dependency:
@@ -526,15 +528,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase =>
526528
val typeRef = tree.tpe
527529
val clazzValue = clsOf(typeRef)
528530
val superClassCtor = genJSConstructorOf(tree, extractSuperTypeConstructor(tree.rhs))
529-
val fakeNewInstances = {
530-
/* We need to use `reverse` because the Scope returns elements in reverse order compared to tree definitions.
531-
* The back-end needs the fake News to be in the same order as the corresponding tree definitions.
532-
*/
533-
val ctors = cls.info.decls.lookupAll(nme.CONSTRUCTOR).toList.reverse
534-
val elems = ctors.map(ctor => fakeNew(cls, ctor.asTerm))
535-
JavaSeqLiteral(elems, TypeTree(defn.AnyRefType))
536-
}
537-
ref(jsdefn.Runtime_createLocalJSClass).appliedTo(clazzValue, superClassCtor, fakeNewInstances)
531+
ref(jsdefn.Runtime_createLocalJSClass).appliedTo(clazzValue, superClassCtor, ref(defn.Predef_undefined))
538532
}
539533

540534
val jsclassVal = state.localClass2jsclassVal(sym)
@@ -556,23 +550,6 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase =>
556550
}
557551
}
558552

559-
/** Creates a fake invocation of the the given class with the given constructor. */
560-
def fakeNew(cls: ClassSymbol, ctor: TermSymbol)(using Context): Tree = {
561-
/* TODO This is not entirely good enough, as it break -Ycheck for generic
562-
* classes. Erasure restores the consistency of the fake invocations.
563-
* Improving this is left for later.
564-
*/
565-
566-
val tycon = cls.typeRef
567-
val targs = cls.typeParams.map(_ => TypeBounds.emptyPolyKind)
568-
val argss = ctor.info.paramInfoss.map(_.map(_ => ref(defn.Predef_undefined)))
569-
570-
New(tycon)
571-
.select(TermRef(tycon, ctor))
572-
.appliedToTypes(targs)
573-
.appliedToArgss(argss)
574-
}
575-
576553
// This method, together with transformTypeApply and transformSelect, implements step (E)
577554
override def transformApply(tree: Apply)(using Context): Tree = {
578555
if (!isFullyApplied(tree)) {

project/Build.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,9 @@ object Build {
10091009
scalaJSLinkerConfig ~= { _.withSemantics(build.TestSuiteLinkerOptions.semantics _) },
10101010
scalaJSModuleInitializers in Test ++= build.TestSuiteLinkerOptions.moduleInitializers,
10111011

1012+
// Perform Ycheck after the Scala.js-specific transformation phases
1013+
scalacOptions += "-Ycheck:explicitJSClasses,addLocalJSFakeNews",
1014+
10121015
jsEnvInput in Test := {
10131016
val resourceDir = fetchScalaJSSource.value / "test-suite/js/src/test/resources"
10141017
val f = (resourceDir / "NonNativeJSTypeTestNatives.js").toPath

0 commit comments

Comments
 (0)