Skip to content

Commit b7256bb

Browse files
authored
Merge pull request #9774 from dotty-staging/sjs-non-native-js-classes
Scala.js: Implement non-native JS classes.
2 parents 119e1de + a91beae commit b7256bb

17 files changed

+2894
-270
lines changed

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

Lines changed: 762 additions & 190 deletions
Large diffs are not rendered by default.
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
package dotty.tools.backend.sjs
2+
3+
import org.scalajs.ir
4+
import org.scalajs.ir.{Position, Trees => js, Types => jstpe}
5+
import org.scalajs.ir.Names._
6+
import org.scalajs.ir.OriginalName.NoOriginalName
7+
8+
import JSCodeGen.UndefinedParam
9+
10+
object JSConstructorGen {
11+
12+
/** Builds one JS constructor out of several "init" methods and their
13+
* dispatcher.
14+
*
15+
* This method and the rest of this file are copied verbatim from `GenJSCode`
16+
* for scalac, since there is no dependency on the compiler trees/symbols/etc.
17+
* We are only manipulating IR trees and types.
18+
*
19+
* The only difference is the two parameters `overloadIdent` and `reportError`,
20+
* which are added so that this entire file can be even more isolated.
21+
*/
22+
def buildJSConstructorDef(dispatch: js.JSMethodDef, ctors: List[js.MethodDef],
23+
overloadIdent: js.LocalIdent)(
24+
reportError: String => Unit)(
25+
implicit pos: Position): js.JSMethodDef = {
26+
27+
val js.JSMethodDef(_, dispatchName, dispatchArgs, dispatchResolution) =
28+
dispatch
29+
30+
val jsConstructorBuilder = mkJSConstructorBuilder(ctors, reportError)
31+
32+
// Section containing the overload resolution and casts of parameters
33+
val overloadSelection = mkOverloadSelection(jsConstructorBuilder,
34+
overloadIdent, dispatchResolution)
35+
36+
/* Section containing all the code executed before the call to `this`
37+
* for every secondary constructor.
38+
*/
39+
val prePrimaryCtorBody =
40+
jsConstructorBuilder.mkPrePrimaryCtorBody(overloadIdent)
41+
42+
val primaryCtorBody = jsConstructorBuilder.primaryCtorBody
43+
44+
/* Section containing all the code executed after the call to this for
45+
* every secondary constructor.
46+
*/
47+
val postPrimaryCtorBody =
48+
jsConstructorBuilder.mkPostPrimaryCtorBody(overloadIdent)
49+
50+
val newBody = js.Block(overloadSelection ::: prePrimaryCtorBody ::
51+
primaryCtorBody :: postPrimaryCtorBody :: js.Undefined() :: Nil)
52+
53+
js.JSMethodDef(js.MemberFlags.empty, dispatchName, dispatchArgs, newBody)(
54+
dispatch.optimizerHints, None)
55+
}
56+
57+
private class ConstructorTree(val overrideNum: Int, val method: js.MethodDef,
58+
val subConstructors: List[ConstructorTree]) {
59+
60+
lazy val overrideNumBounds: (Int, Int) =
61+
if (subConstructors.isEmpty) (overrideNum, overrideNum)
62+
else (subConstructors.head.overrideNumBounds._1, overrideNum)
63+
64+
def get(methodName: MethodName): Option[ConstructorTree] = {
65+
if (methodName == this.method.methodName) {
66+
Some(this)
67+
} else {
68+
subConstructors.iterator.map(_.get(methodName)).collectFirst {
69+
case Some(node) => node
70+
}
71+
}
72+
}
73+
74+
def getParamRefs(implicit pos: Position): List[js.VarRef] =
75+
method.args.map(_.ref)
76+
77+
def getAllParamDefsAsVars(implicit pos: Position): List[js.VarDef] = {
78+
val localDefs = method.args.map { pDef =>
79+
js.VarDef(pDef.name, pDef.originalName, pDef.ptpe, mutable = true,
80+
jstpe.zeroOf(pDef.ptpe))
81+
}
82+
localDefs ++ subConstructors.flatMap(_.getAllParamDefsAsVars)
83+
}
84+
}
85+
86+
private class JSConstructorBuilder(root: ConstructorTree, reportError: String => Unit) {
87+
88+
def primaryCtorBody: js.Tree = root.method.body.getOrElse(
89+
throw new AssertionError("Found abstract constructor"))
90+
91+
def hasSubConstructors: Boolean = root.subConstructors.nonEmpty
92+
93+
def getOverrideNum(methodName: MethodName): Int =
94+
root.get(methodName).fold(-1)(_.overrideNum)
95+
96+
def getParamRefsFor(methodName: MethodName)(implicit pos: Position): List[js.VarRef] =
97+
root.get(methodName).fold(List.empty[js.VarRef])(_.getParamRefs)
98+
99+
def getAllParamDefsAsVars(implicit pos: Position): List[js.VarDef] =
100+
root.getAllParamDefsAsVars
101+
102+
def mkPrePrimaryCtorBody(overrideNumIdent: js.LocalIdent)(
103+
implicit pos: Position): js.Tree = {
104+
val overrideNumRef = js.VarRef(overrideNumIdent)(jstpe.IntType)
105+
mkSubPreCalls(root, overrideNumRef)
106+
}
107+
108+
def mkPostPrimaryCtorBody(overrideNumIdent: js.LocalIdent)(
109+
implicit pos: Position): js.Tree = {
110+
val overrideNumRef = js.VarRef(overrideNumIdent)(jstpe.IntType)
111+
js.Block(mkSubPostCalls(root, overrideNumRef))
112+
}
113+
114+
private def mkSubPreCalls(constructorTree: ConstructorTree,
115+
overrideNumRef: js.VarRef)(implicit pos: Position): js.Tree = {
116+
val overrideNumss = constructorTree.subConstructors.map(_.overrideNumBounds)
117+
val paramRefs = constructorTree.getParamRefs
118+
val bodies = constructorTree.subConstructors.map { constructorTree =>
119+
mkPrePrimaryCtorBodyOnSndCtr(constructorTree, overrideNumRef, paramRefs)
120+
}
121+
overrideNumss.zip(bodies).foldRight[js.Tree](js.Skip()) {
122+
case ((numBounds, body), acc) =>
123+
val cond = mkOverrideNumsCond(overrideNumRef, numBounds)
124+
js.If(cond, body, acc)(jstpe.BooleanType)
125+
}
126+
}
127+
128+
private def mkPrePrimaryCtorBodyOnSndCtr(constructorTree: ConstructorTree,
129+
overrideNumRef: js.VarRef, outputParams: List[js.VarRef])(
130+
implicit pos: Position): js.Tree = {
131+
val subCalls =
132+
mkSubPreCalls(constructorTree, overrideNumRef)
133+
134+
val preSuperCall = {
135+
def checkForUndefinedParams(args: List[js.Tree]): List[js.Tree] = {
136+
def isUndefinedParam(tree: js.Tree): Boolean = tree match {
137+
case js.Transient(UndefinedParam) => true
138+
case _ => false
139+
}
140+
141+
if (!args.exists(isUndefinedParam)) {
142+
args
143+
} else {
144+
/* If we find an undefined param here, we're in trouble, because
145+
* the handling of a default param for the target constructor has
146+
* already been done during overload resolution. If we store an
147+
* `undefined` now, it will fall through without being properly
148+
* processed.
149+
*
150+
* Since this seems very tricky to deal with, and a pretty rare
151+
* use case (with a workaround), we emit an "implementation
152+
* restriction" error.
153+
*/
154+
reportError(
155+
"Implementation restriction: in a JS class, a secondary " +
156+
"constructor calling another constructor with default " +
157+
"parameters must provide the values of all parameters.")
158+
159+
/* Replace undefined params by undefined to prevent subsequent
160+
* compiler crashes.
161+
*/
162+
args.map { arg =>
163+
if (isUndefinedParam(arg))
164+
js.Undefined()(arg.pos)
165+
else
166+
arg
167+
}
168+
}
169+
}
170+
171+
constructorTree.method.body.get match {
172+
case js.Block(stats) =>
173+
val beforeSuperCall = stats.takeWhile {
174+
case js.ApplyStatic(_, _, mtd, _) => !mtd.name.isConstructor
175+
case _ => true
176+
}
177+
val superCallParams = stats.collectFirst {
178+
case js.ApplyStatic(_, _, mtd, js.This() :: args)
179+
if mtd.name.isConstructor =>
180+
val checkedArgs = checkForUndefinedParams(args)
181+
zipMap(outputParams, checkedArgs)(js.Assign(_, _))
182+
}.getOrElse(Nil)
183+
184+
beforeSuperCall ::: superCallParams
185+
186+
case js.ApplyStatic(_, _, mtd, js.This() :: args)
187+
if mtd.name.isConstructor =>
188+
val checkedArgs = checkForUndefinedParams(args)
189+
zipMap(outputParams, checkedArgs)(js.Assign(_, _))
190+
191+
case _ => Nil
192+
}
193+
}
194+
195+
js.Block(subCalls :: preSuperCall)
196+
}
197+
198+
private def mkSubPostCalls(constructorTree: ConstructorTree,
199+
overrideNumRef: js.VarRef)(implicit pos: Position): js.Tree = {
200+
val overrideNumss = constructorTree.subConstructors.map(_.overrideNumBounds)
201+
val bodies = constructorTree.subConstructors.map { ct =>
202+
mkPostPrimaryCtorBodyOnSndCtr(ct, overrideNumRef)
203+
}
204+
overrideNumss.zip(bodies).foldRight[js.Tree](js.Skip()) {
205+
case ((numBounds, js.Skip()), acc) => acc
206+
207+
case ((numBounds, body), acc) =>
208+
val cond = mkOverrideNumsCond(overrideNumRef, numBounds)
209+
js.If(cond, body, acc)(jstpe.BooleanType)
210+
}
211+
}
212+
213+
private def mkPostPrimaryCtorBodyOnSndCtr(constructorTree: ConstructorTree,
214+
overrideNumRef: js.VarRef)(implicit pos: Position): js.Tree = {
215+
val postSuperCall = {
216+
constructorTree.method.body.get match {
217+
case js.Block(stats) =>
218+
stats.dropWhile {
219+
case js.ApplyStatic(_, _, mtd, _) => !mtd.name.isConstructor
220+
case _ => true
221+
}.tail
222+
223+
case _ => Nil
224+
}
225+
}
226+
js.Block(postSuperCall :+ mkSubPostCalls(constructorTree, overrideNumRef))
227+
}
228+
229+
private def mkOverrideNumsCond(numRef: js.VarRef,
230+
numBounds: (Int, Int))(implicit pos: Position) = numBounds match {
231+
case (lo, hi) if lo == hi =>
232+
js.BinaryOp(js.BinaryOp.Int_==, js.IntLiteral(lo), numRef)
233+
234+
case (lo, hi) if lo == hi - 1 =>
235+
val lhs = js.BinaryOp(js.BinaryOp.Int_==, numRef, js.IntLiteral(lo))
236+
val rhs = js.BinaryOp(js.BinaryOp.Int_==, numRef, js.IntLiteral(hi))
237+
js.If(lhs, js.BooleanLiteral(true), rhs)(jstpe.BooleanType)
238+
239+
case (lo, hi) =>
240+
val lhs = js.BinaryOp(js.BinaryOp.Int_<=, js.IntLiteral(lo), numRef)
241+
val rhs = js.BinaryOp(js.BinaryOp.Int_<=, numRef, js.IntLiteral(hi))
242+
js.BinaryOp(js.BinaryOp.Boolean_&, lhs, rhs)
243+
js.If(lhs, rhs, js.BooleanLiteral(false))(jstpe.BooleanType)
244+
}
245+
}
246+
247+
private def zipMap[T, U, V](xs: List[T], ys: List[U])(
248+
f: (T, U) => V): List[V] = {
249+
for ((x, y) <- xs zip ys) yield f(x, y)
250+
}
251+
252+
/** mkOverloadSelection return a list of `stats` with that starts with:
253+
* 1) The definition for the local variable that will hold the overload
254+
* resolution number.
255+
* 2) The definitions of all local variables that are used as parameters
256+
* in all the constructors.
257+
* 3) The overload resolution match/if statements. For each overload the
258+
* overload number is assigned and the parameters are cast and assigned
259+
* to their corresponding variables.
260+
*/
261+
private def mkOverloadSelection(jsConstructorBuilder: JSConstructorBuilder,
262+
overloadIdent: js.LocalIdent, dispatchResolution: js.Tree)(
263+
implicit pos: Position): List[js.Tree] = {
264+
265+
def deconstructApplyCtor(body: js.Tree): (List[js.Tree], MethodName, List[js.Tree]) = {
266+
val (prepStats, applyCtor) = (body: @unchecked) match {
267+
case applyCtor: js.ApplyStatic =>
268+
(Nil, applyCtor)
269+
case js.Block(prepStats :+ (applyCtor: js.ApplyStatic)) =>
270+
(prepStats, applyCtor)
271+
}
272+
val js.ApplyStatic(_, _, js.MethodIdent(ctorName), js.This() :: ctorArgs) =
273+
applyCtor
274+
assert(ctorName.isConstructor,
275+
s"unexpected super constructor call to non-constructor $ctorName at ${applyCtor.pos}")
276+
(prepStats, ctorName, ctorArgs)
277+
}
278+
279+
if (!jsConstructorBuilder.hasSubConstructors) {
280+
val (prepStats, ctorName, ctorArgs) =
281+
deconstructApplyCtor(dispatchResolution)
282+
283+
val refs = jsConstructorBuilder.getParamRefsFor(ctorName)
284+
assert(refs.size == ctorArgs.size, s"at $pos")
285+
val assignCtorParams = zipMap(refs, ctorArgs) { (ref, ctorArg) =>
286+
js.VarDef(ref.ident, NoOriginalName, ref.tpe, mutable = false, ctorArg)
287+
}
288+
289+
prepStats ::: assignCtorParams
290+
} else {
291+
val overloadRef = js.VarRef(overloadIdent)(jstpe.IntType)
292+
293+
/* transformDispatch takes the body of the method generated by
294+
* `genJSConstructorDispatch` and transform it recursively.
295+
*/
296+
def transformDispatch(tree: js.Tree): js.Tree = tree match {
297+
// Parameter count resolution
298+
case js.Match(selector, cases, default) =>
299+
val newCases = cases.map {
300+
case (literals, body) => (literals, transformDispatch(body))
301+
}
302+
val newDefault = transformDispatch(default)
303+
js.Match(selector, newCases, newDefault)(tree.tpe)
304+
305+
// Parameter type resolution
306+
case js.If(cond, thenp, elsep) =>
307+
js.If(cond, transformDispatch(thenp),
308+
transformDispatch(elsep))(tree.tpe)
309+
310+
// Throw(StringLiteral(No matching overload))
311+
case tree: js.Throw =>
312+
tree
313+
314+
// Overload resolution done, apply the constructor
315+
case _ =>
316+
val (prepStats, ctorName, ctorArgs) = deconstructApplyCtor(tree)
317+
318+
val num = jsConstructorBuilder.getOverrideNum(ctorName)
319+
val overloadAssign = js.Assign(overloadRef, js.IntLiteral(num))
320+
321+
val refs = jsConstructorBuilder.getParamRefsFor(ctorName)
322+
assert(refs.size == ctorArgs.size, s"at $pos")
323+
val assignCtorParams = zipMap(refs, ctorArgs)(js.Assign(_, _))
324+
325+
js.Block(overloadAssign :: prepStats ::: assignCtorParams)
326+
}
327+
328+
val newDispatchResolution = transformDispatch(dispatchResolution)
329+
val allParamDefsAsVars = jsConstructorBuilder.getAllParamDefsAsVars
330+
val overrideNumDef = js.VarDef(overloadIdent, NoOriginalName,
331+
jstpe.IntType, mutable = true, js.IntLiteral(0))
332+
333+
overrideNumDef :: allParamDefsAsVars ::: newDispatchResolution :: Nil
334+
}
335+
}
336+
337+
private def mkJSConstructorBuilder(ctors: List[js.MethodDef], reportError: String => Unit)(
338+
implicit pos: Position): JSConstructorBuilder = {
339+
def findCtorForwarderCall(tree: js.Tree): MethodName = (tree: @unchecked) match {
340+
case js.ApplyStatic(_, _, method, js.This() :: _)
341+
if method.name.isConstructor =>
342+
method.name
343+
344+
case js.Block(stats) =>
345+
stats.collectFirst {
346+
case js.ApplyStatic(_, _, method, js.This() :: _)
347+
if method.name.isConstructor =>
348+
method.name
349+
}.get
350+
}
351+
352+
val (primaryCtor :: Nil, secondaryCtors) = ctors.partition {
353+
_.body.get match {
354+
case js.Block(stats) =>
355+
stats.exists(_.isInstanceOf[js.JSSuperConstructorCall])
356+
357+
case _: js.JSSuperConstructorCall => true
358+
case _ => false
359+
}
360+
}
361+
362+
val ctorToChildren = secondaryCtors.map { ctor =>
363+
findCtorForwarderCall(ctor.body.get) -> ctor
364+
}.groupBy(_._1).map(kv => kv._1 -> kv._2.map(_._2)).withDefaultValue(Nil)
365+
366+
var overrideNum = -1
367+
def mkConstructorTree(method: js.MethodDef): ConstructorTree = {
368+
val subCtrTrees = ctorToChildren(method.methodName).map(mkConstructorTree)
369+
overrideNum += 1
370+
new ConstructorTree(overrideNum, method, subCtrTrees)
371+
}
372+
373+
new JSConstructorBuilder(mkConstructorTree(primaryCtor), reportError: String => Unit)
374+
}
375+
376+
}

0 commit comments

Comments
 (0)