From c92fa09369352ed18adbd70ec0f60b242961fa24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 18 Sep 2020 15:23:39 +0200 Subject: [PATCH 01/11] Scala.js: Implement non-native JS classes. This commit implements the entire specification of non-native JS classes, including nested ones, except the behavior of *anonymous* JS classes. Anonymous JS classes are not supposed to have their own class/prototype, but currently they still do. The most important PRs to scala-js/scala-js that define what is implemented here were: 1. https://github.com/scala-js/scala-js/pull/1809 Essentials of Scala.js-defined classes (former name of non-native JS classes) 2. https://github.com/scala-js/scala-js/pull/1982 Support for secondary constructors 3. https://github.com/scala-js/scala-js/pull/2186 Support for default parameters in constructors 4. https://github.com/scala-js/scala-js/pull/2659 Support for `js.Symbol`s in `@JSName` annotations 5. https://github.com/scala-js/scala-js/pull/3161 Nested JS classes However, this commit was written more based on the state of things at Scala.js v1.2.0 rather than as a port of the abovementioned PRs. The support is spread over 4 components: * The `ExplicitJSClasses` phase, which reifies all nested JS classes, and has extensive documentation in the code. * The `JSExportsGen` component of the back-end, which creates dispatchers for run-time overloading, default parameters and variadic arguments (equivalent to `GenJSExports` in Scala 2). * The `JSConstructorGen` component, which massages the constructors of JS classes and their dispatcher into a unique JS constructor. * Bits and pieces in `JSCodeGen`, notably to generate the `js.ClassDef`s for non-native JS classes, orchestrate the two other back-end components, and to adapt calls to the members of non-native JS classes. `JSConstructorGen` in particular is copied quasi-verbatim from pieces of `GenJSCode` in Scala 2, since it works on `js.IRNode`s, without knowledge of scalac/dotc data structures. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 660 +++++++++---- .../tools/backend/sjs/JSConstructorGen.scala | 376 +++++++ .../tools/backend/sjs/JSDefinitions.scala | 10 + .../dotty/tools/backend/sjs/JSEncoding.scala | 22 + .../tools/backend/sjs/JSExportsGen.scala | 914 ++++++++++++++++++ .../tools/backend/sjs/JSPrimitives.scala | 12 +- .../dotty/tools/dotc/core/TypeErasure.scala | 2 + .../tools/dotc/transform/Constructors.scala | 5 +- .../transform/sjs/ExplicitJSClasses.scala | 703 +++++++++++++- .../tools/dotc/transform/sjs/JSSymUtils.scala | 29 +- .../dotc/transform/sjs/PrepJSInterop.scala | 13 +- .../src/dotty/tools/dotc/util/HashSet.scala | 7 + project/Build.scala | 35 +- 13 files changed, 2552 insertions(+), 236 deletions(-) create mode 100644 compiler/src/dotty/tools/backend/sjs/JSConstructorGen.scala create mode 100644 compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index cd7b1ba1ce26..10f421c31e2b 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -64,32 +64,46 @@ class JSCodeGen()(using genCtx: Context) { import JSCodeGen._ import tpd._ - private val sjsPlatform = dotty.tools.dotc.config.SJSPlatform.sjsPlatform - private val jsdefn = JSDefinitions.jsdefn + val sjsPlatform = dotty.tools.dotc.config.SJSPlatform.sjsPlatform + val jsdefn = JSDefinitions.jsdefn private val primitives = new JSPrimitives(genCtx) - private val positionConversions = new JSPositions()(using genCtx) + val positionConversions = new JSPositions()(using genCtx) import positionConversions._ + private val jsExportsGen = new JSExportsGen(this) + // Some state -------------------------------------------------------------- private val generatedClasses = mutable.ListBuffer.empty[js.ClassDef] private val generatedStaticForwarderClasses = mutable.ListBuffer.empty[(Symbol, js.ClassDef)] - private val currentClassSym = new ScopedVar[Symbol] + val currentClassSym = new ScopedVar[Symbol] private val currentMethodSym = new ScopedVar[Symbol] private val localNames = new ScopedVar[LocalNameGenerator] private val thisLocalVarIdent = new ScopedVar[Option[js.LocalIdent]] private val undefinedDefaultParams = new ScopedVar[mutable.Set[Symbol]] - private def withNewLocalNameScope[A](body: => A): A = { + /* Contextual JS class value for some operations of nested JS classes that need one. */ + private val contextualJSClassValue = new ScopedVar[Option[js.Tree]](None) + + private def acquireContextualJSClassValue[A](f: Option[js.Tree] => A): A = { + val jsClassValue = contextualJSClassValue.get + withScopedVars( + contextualJSClassValue := None + ) { + f(jsClassValue) + } + } + + def withNewLocalNameScope[A](body: => A): A = { withScopedVars(localNames := new LocalNameGenerator) { body } } /** Implicitly materializes the current local name generator. */ - private implicit def implicitLocalNames: LocalNameGenerator = localNames.get + implicit def implicitLocalNames: LocalNameGenerator = localNames.get /* See genSuperCall() * TODO Can we avoid this unscoped var? @@ -103,7 +117,7 @@ class JSCodeGen()(using genCtx: Context) { localNames.get.freshLocalIdent() /** Returns a new fresh local identifier. */ - private def freshLocalIdent(base: String)(implicit pos: Position): js.LocalIdent = + def freshLocalIdent(base: String)(implicit pos: Position): js.LocalIdent = localNames.get.freshLocalIdent(base) /** Returns a new fresh local identifier. */ @@ -135,10 +149,10 @@ class JSCodeGen()(using genCtx: Context) { * function in the method that instantiates it. * * Other ClassDefs are emitted according to their nature: - * * Scala.js-defined JS class -> `genScalaJSDefinedJSClass()` - * * Other raw JS type (<: js.Any) -> `genRawJSClassData()` - * * Interface -> `genInterface()` - * * Normal class -> `genClass()` + * * Non-native JS class -> `genNonNativeJSClass()` + * * Other JS type (<: js.Any) -> `genRawJSClassData()` + * * Interface -> `genInterface()` + * * Normal class -> `genClass()` */ private def genCompilationUnit(cunit: CompilationUnit): Unit = { def collectTypeDefs(tree: Tree): List[TypeDef] = { @@ -169,8 +183,8 @@ class JSCodeGen()(using genCtx: Context) { val tree = if (isJSType(sym)) { /*assert(!isRawJSFunctionDef(sym), s"Raw JS function def should have been recorded: $cd")*/ - if (!sym.is(Trait) && isScalaJSDefinedJSClass(sym)) - genScalaJSDefinedJSClass(td) + if (!sym.is(Trait) && sym.isNonNativeJSClass) + genNonNativeJSClass(td) else genRawJSClassData(td) } else if (sym.is(Trait)) { @@ -410,7 +424,7 @@ class JSCodeGen()(using genCtx: Context) { kind, None, Some(encodeClassNameIdent(sym.superClass)), - genClassInterfaces(sym), + genClassInterfaces(sym, forJSClass = false), None, None, hashedDefs, @@ -421,32 +435,97 @@ class JSCodeGen()(using genCtx: Context) { } /** Gen the IR ClassDef for a Scala.js-defined JS class. */ - private def genScalaJSDefinedJSClass(td: TypeDef): js.ClassDef = { + private def genNonNativeJSClass(td: TypeDef): js.ClassDef = { val sym = td.symbol.asClass implicit val pos: SourcePosition = sym.sourcePos - assert(!sym.is(Trait), - "genScalaJSDefinedJSClass() must be called only for normal classes: "+sym) + assert(sym.isNonNativeJSClass, + i"genNonNativeJSClass() must be called only for non-native JS classes: $sym") assert(sym.superClass != NoSymbol, sym) val classIdent = encodeClassNameIdent(sym) val originalName = originalNameOfClass(sym) - report.error("cannot emit non-native JS classes yet", td.sourcePos) + // Generate members (constructor + methods) + + val constructorTrees = new mutable.ListBuffer[DefDef] + val generatedMethods = new mutable.ListBuffer[js.MethodDef] + val dispatchMethodNames = new mutable.ListBuffer[JSName] - // Dummy result - js.ClassDef( + val tpl = td.rhs.asInstanceOf[Template] + for (tree <- tpl.constr :: tpl.body) { + tree match { + case EmptyTree => () + + case _: ValDef => + () // fields are added via genClassFields() + + case dd: DefDef => + val sym = dd.symbol + val exposed = sym.isJSExposed + + if (sym.isClassConstructor) { + constructorTrees += dd + } else if (exposed && sym.is(Accessor, butNot = Lazy)) { + // Exposed accessors must not be emitted, since the field they access is enough. + } else if (sym.hasAnnotation(jsdefn.JSOptionalAnnot)) { + // Optional methods must not be emitted + } else { + generatedMethods ++= genMethod(dd) + + // Collect the names of the dispatchers we have to create + if (exposed && !sym.is(Deferred)) { + /* We add symbols that we have to expose here. This way we also + * get inherited stuff that is implemented in this class. + */ + dispatchMethodNames += jsNameOf(sym) + } + } + + case _ => + throw new FatalError("Illegal tree in gen of genNonNativeJSClass(): " + tree) + } + } + + val (jsClassCaptures, generatedConstructor) = + genJSClassCapturesAndConstructor(sym, constructorTrees.toList) + + /* If there is one, the JS super class value is always the first JS class + * capture. This is a JSCodeGen-specific invariant (the IR does not rely + * on this) enforced in genJSClassCapturesAndConstructor. + */ + val jsSuperClass = jsClassCaptures.map(_.head.ref) + + // Generate fields (and add to methods + ctors) + val generatedMembers = { + genClassFields(td) ::: + generatedConstructor :: + jsExportsGen.genJSClassDispatchers(sym, dispatchMethodNames.result().distinct) ::: + generatedMethods.toList + } + + // Hashed definitions of the class + val hashedMemberDefs = ir.Hashers.hashMemberDefs(generatedMembers) + + // The complete class definition + val kind = + if (isStaticModule(sym)) ClassKind.JSModuleClass + else ClassKind.JSClass + + val classDefinition = js.ClassDef( classIdent, - originalName, - ClassKind.JSClass, - None, + originalNameOfClass(sym), + kind, + jsClassCaptures, Some(encodeClassNameIdent(sym.superClass)), - genClassInterfaces(sym), - None, + genClassInterfaces(sym, forJSClass = true), + jsSuperClass, None, - Nil, + hashedMemberDefs, Nil)( OptimizerHints.empty) + + classDefinition } /** Gen the IR ClassDef for a raw JS class or trait. @@ -472,7 +551,7 @@ class JSCodeGen()(using genCtx: Context) { kind, None, superClass, - genClassInterfaces(sym), + genClassInterfaces(sym, forJSClass = false), None, jsNativeLoadSpec, Nil, @@ -503,7 +582,7 @@ class JSCodeGen()(using genCtx: Context) { } } - val superInterfaces = genClassInterfaces(sym) + val superInterfaces = genClassInterfaces(sym, forJSClass = false) val genMethodsList = generatedMethods.toList val allMemberDefs = @@ -527,10 +606,11 @@ class JSCodeGen()(using genCtx: Context) { OptimizerHints.empty) } - private def genClassInterfaces(sym: ClassSymbol)( + private def genClassInterfaces(sym: ClassSymbol, forJSClass: Boolean)( implicit pos: Position): List[js.ClassIdent] = { for { intf <- sym.directlyInheritedTraits + if !(forJSClass && intf == defn.DynamicClass) } yield { encodeClassNameIdent(intf) } @@ -668,64 +748,59 @@ class JSCodeGen()(using genCtx: Context) { // Generate the fields of a class ------------------------------------------ - /** Gen definitions for the fields of a class. - */ - private def genClassFields(td: TypeDef): List[js.FieldDef] = { + /** Gen definitions for the fields of a class. */ + private def genClassFields(td: TypeDef): List[js.AnyFieldDef] = { val classSym = td.symbol.asClass assert(currentClassSym.get == classSym, "genClassFields called with a ClassDef other than the current one") + val isJSClass = classSym.isNonNativeJSClass + // Term members that are neither methods nor modules are fields classSym.info.decls.filter { f => !f.isOneOf(Method | Module) && f.isTerm && !f.hasAnnotation(jsdefn.JSNativeAnnot) + && !f.hasAnnotation(jsdefn.JSOptionalAnnot) }.map({ f => implicit val pos = f.span - val name = - /*if (isExposed(f)) js.StringLiteral(jsNameOf(f)) - else*/ encodeFieldSym(f) + val flags = js.MemberFlags.empty.withMutable(f.is(Mutable)) - val irTpe = //if (!isScalaJSDefinedJSClass(classSym)) { - toIRType(f.info) - /*} else { - val tpeEnteringPosterasure = - enteringPhase(currentRun.posterasurePhase)(f.tpe) - tpeEnteringPosterasure match { - case tpe: ErasedValueType => - /* Here, we must store the field as the boxed representation of - * the value class. The default value of that field, as - * initialized at the time the instance is created, will - * therefore be null. This will not match the behavior we would - * get in a Scala class. To match the behavior, we would need to - * initialized to an instance of the boxed representation, with - * an underlying value set to the zero of its type. However we - * cannot implement that, so we live with the discrepancy. - * Anyway, scalac also has problems with uninitialized value - * class values, if they come from a generic context. - * - * TODO Evaluate how much of this needs to be adapted for dotc, - * which unboxes `null` to the zero of their underlying. - */ - jstpe.ClassType(encodeClassFullName(tpe.valueClazz)) + val irTpe = + if (isJSClass) genExposedFieldIRType(f) + else toIRType(f.info) - case _ if f.tpe.typeSymbol == CharClass => - /* Will be initialized to null, which will unbox to '\0' when - * read. - */ - jstpe.ClassType(ir.Definitions.BoxedCharacterClass) + if (isJSClass && f.isJSExposed) + js.JSFieldDef(flags, genExpr(f.jsName)(f.sourcePos), irTpe) + else + js.FieldDef(flags, encodeFieldSym(f), originalNameOfField(f), irTpe) + }).toList + } - case _ => - /* Other types are not boxed, so we can initialized them to - * their true zero. - */ - toIRType(f.tpe) - } - }*/ + private def genExposedFieldIRType(f: Symbol): jstpe.Type = { + val tpeEnteringPosterasure = atPhase(elimErasedValueTypePhase)(f.info) + tpeEnteringPosterasure match { + case tpe: ErasedValueType => + /* Here, we must store the field as the boxed representation of + * the value class. The default value of that field, as + * initialized at the time the instance is created, will + * therefore be null. This will not match the behavior we would + * get in a Scala class. To match the behavior, we would need to + * initialized to an instance of the boxed representation, with + * an underlying value set to the zero of its type. However we + * cannot implement that, so we live with the discrepancy. + * Anyway, scalac also has problems with uninitialized value + * class values, if they come from a generic context. + * + * TODO Evaluate how much of this needs to be adapted for dotc, + * which unboxes `null` to the zero of their underlying. + */ + jstpe.ClassType(encodeClassName(tpe.valueClassSymbol)) - val flags = js.MemberFlags.empty.withMutable(f.is(Mutable)) - js.FieldDef(flags, name, originalNameOfField(f), irTpe) - }).toList + case _ => + // Other types are not boxed, so we can initialized them to their true zero. + toIRType(f.info) + } } // Static initializers ----------------------------------------------------- @@ -815,6 +890,52 @@ class JSCodeGen()(using genCtx: Context) { } } + // Constructor of a non-native JS class ------------------------------------ + + def genJSClassCapturesAndConstructor(classSym: Symbol, + constructorTrees: List[DefDef]): (Option[List[js.ParamDef]], js.JSMethodDef) = { + implicit val pos = classSym.span + + if (hasDefaultCtorArgsAndJSModule(classSym)) { + report.error( + "Implementation restriction: constructors of " + + "non-native JS classes cannot have default parameters " + + "if their companion module is JS native.", + classSym.srcPos) + val ctorDef = js.JSMethodDef(js.MemberFlags.empty, + js.StringLiteral("constructor"), Nil, js.Skip())( + OptimizerHints.empty, None) + (None, ctorDef) + } else { + withNewLocalNameScope { + localNames.reserveLocalName(JSSuperClassParamName) + + val ctors: List[js.MethodDef] = constructorTrees.flatMap { tree => + genMethodWithCurrentLocalNameScope(tree) + } + + val (captureParams, dispatch) = + jsExportsGen.genJSConstructorDispatch(constructorTrees.map(_.symbol)) + + /* Ensure that the first JS class capture is a reference to the JS + * super class value. genNonNativeJSClass relies on this. + */ + val captureParamsWithJSSuperClass = captureParams.map { params => + val jsSuperClassParam = js.ParamDef( + js.LocalIdent(JSSuperClassParamName), NoOriginalName, + jstpe.AnyType, mutable = false, rest = false) + jsSuperClassParam :: params + } + + val ctorDef = JSConstructorGen.buildJSConstructorDef(dispatch, ctors, freshLocalIdent("overload")) { + msg => report.error(msg, classSym.srcPos) + } + + (captureParamsWithJSSuperClass, ctorDef) + } + } + } + // Generate a method ------------------------------------------------------- /** Generates the JSNativeMemberDef. */ @@ -867,16 +988,12 @@ class JSCodeGen()(using genCtx: Context) { val params = if (vparamss.isEmpty) Nil else vparamss.head.map(_.symbol) val isJSClassConstructor = - sym.isClassConstructor && isScalaJSDefinedJSClass(currentClassSym) + sym.isClassConstructor && currentClassSym.isNonNativeJSClass val methodName = encodeMethodSym(sym) val originalName = originalNameOfMethod(sym) - def jsParams = for (param <- params) yield { - implicit val pos = param.span - js.ParamDef(encodeLocalSym(param), originalNameOfLocal(param), - toIRType(param.info), mutable = false, rest = false) - } + def jsParams = params.map(genParamDef(_)) if (primitives.isPrimitive(sym)) { None @@ -911,14 +1028,14 @@ class JSCodeGen()(using genCtx: Context) { } val methodDef = { - /*if (isJSClassConstructor) { + if (isJSClassConstructor) { val body0 = genStat(rhs) val body1 = if (!sym.isPrimaryConstructor) body0 else moveAllStatementsAfterSuperConstructorCall(body0) - js.MethodDef(js.MemberFlags.empty, methodName, - jsParams, jstpe.NoType, body1)(optimizerHints, None) - } else*/ if (sym.isClassConstructor) { + js.MethodDef(js.MemberFlags.empty, methodName, originalName, + jsParams, jstpe.NoType, Some(body1))(optimizerHints, None) + } else if (sym.isClassConstructor) { val namespace = js.MemberNamespace.Constructor js.MethodDef(js.MemberFlags.empty.withNamespace(namespace), methodName, originalName, jsParams, jstpe.NoType, @@ -952,39 +1069,82 @@ class JSCodeGen()(using genCtx: Context) { * an explicit parameter for their `this` value. */ private def genMethodDef(namespace: js.MemberNamespace, methodName: js.MethodIdent, - originalName: OriginalName,paramsSyms: List[Symbol], resultIRType: jstpe.Type, + originalName: OriginalName, paramsSyms: List[Symbol], resultIRType: jstpe.Type, tree: Tree, optimizerHints: OptimizerHints): js.MethodDef = { implicit val pos = tree.span - val jsParams = for (param <- paramsSyms) yield { - implicit val pos = param.span - js.ParamDef(encodeLocalSym(param), originalNameOfLocal(param), - toIRType(param.info), mutable = false, rest = false) - } + val jsParams = paramsSyms.map(genParamDef(_)) def genBody() = localNames.makeLabeledIfRequiresEnclosingReturn(resultIRType) { if (resultIRType == jstpe.NoType) genStat(tree) else genExpr(tree) } - //if (!isScalaJSDefinedJSClass(currentClassSym)) { - val flags = js.MemberFlags.empty.withNamespace(namespace) - js.MethodDef(flags, methodName, originalName, jsParams, resultIRType, Some(genBody()))( - optimizerHints, None) - /*} else { + if (!currentClassSym.isNonNativeJSClass) { + val flags = js.MemberFlags.empty.withNamespace(namespace) + js.MethodDef(flags, methodName, originalName, jsParams, resultIRType, Some(genBody()))( + optimizerHints, None) + } else { assert(!namespace.isStatic, tree.span) + val thisLocalIdent = freshLocalIdent("this") withScopedVars( - thisLocalVarIdent := Some(freshLocalIdent("this")) + thisLocalVarIdent := Some(thisLocalIdent) ) { - val thisParamDef = js.ParamDef(thisLocalVarIdent.get.get, + val staticNamespace = + if (namespace.isPrivate) js.MemberNamespace.PrivateStatic + else js.MemberNamespace.PublicStatic + val flags = + js.MemberFlags.empty.withNamespace(staticNamespace) + val thisParamDef = js.ParamDef(thisLocalIdent, thisOriginalName, jstpe.AnyType, mutable = false, rest = false) - js.MethodDef(static = true, methodName, thisParamDef :: jsParams, - resultIRType, genBody())( + js.MethodDef(flags, methodName, originalName, + thisParamDef :: jsParams, resultIRType, Some(genBody()))( optimizerHints, None) } - }*/ + } + } + + /** Moves all statements after the super constructor call. + * + * This is used for the primary constructor of a non-native JS class, + * because those cannot access `this` before the super constructor call. + * + * dotc inserts statements before the super constructor call for param + * accessor initializers (including val's and var's declared in the params). + * We move those after the super constructor call, and are therefore + * executed later than for a Scala class. + */ + private def moveAllStatementsAfterSuperConstructorCall(body: js.Tree): js.Tree = { + val bodyStats = body match { + case js.Block(stats) => stats + case _ => body :: Nil + } + + val (beforeSuper, superCall :: afterSuper) = + bodyStats.span(!_.isInstanceOf[js.JSSuperConstructorCall]) + + assert(!beforeSuper.exists(_.isInstanceOf[js.VarDef]), + s"Trying to move a local VarDef after the super constructor call of a non-native JS class at ${body.pos}") + + js.Block(superCall :: beforeSuper ::: afterSuper)(body.pos) + } + + // ParamDefs --------------------------------------------------------------- + + def genParamDef(sym: Symbol): js.ParamDef = + genParamDef(sym, toIRType(sym.info)) + + private def genParamDef(sym: Symbol, ptpe: jstpe.Type): js.ParamDef = + genParamDef(sym, ptpe, sym.span) + + private def genParamDef(sym: Symbol, pos: Position): js.ParamDef = + genParamDef(sym, toIRType(sym.info), pos) + + private def genParamDef(sym: Symbol, ptpe: jstpe.Type, pos: Position): js.ParamDef = { + js.ParamDef(encodeLocalSym(sym)(implicitly, pos, implicitly), + originalNameOfLocal(sym), ptpe, mutable = false, rest = false)(pos) } // Generate statements and expressions ------------------------------------- @@ -1002,9 +1162,12 @@ class JSCodeGen()(using genCtx: Context) { */ implicit val pos = tree.pos tree match { - case js.Block(stats :+ expr) => js.Block(stats :+ exprToStat(expr)) - case _:js.Literal | js.This() => js.Skip() - case _ => tree + case js.Block(stats :+ expr) => + js.Block(stats :+ exprToStat(expr)) + case _:js.Literal | _:js.This | _:js.VarRef => + js.Skip() + case _ => + tree } } @@ -1017,7 +1180,7 @@ class JSCodeGen()(using genCtx: Context) { result } - private def genExpr(name: JSName)(implicit pos: SourcePosition): js.Tree = name match { + def genExpr(name: JSName)(implicit pos: SourcePosition): js.Tree = name match { case JSName.Literal(name) => js.StringLiteral(name) case JSName.Computed(sym) => genComputedJSName(sym) } @@ -1163,19 +1326,10 @@ class JSCodeGen()(using genCtx: Context) { genLoadModule(sym) } else if (sym.is(JavaStatic)) { genLoadStaticField(sym) - } else /*if (paramAccessorLocals contains sym) { - paramAccessorLocals(sym).ref - } else if (isScalaJSDefinedJSClass(sym.owner)) { - val genQual = genExpr(qualifier) - val boxed = if (isExposed(sym)) - js.JSBracketSelect(genQual, js.StringLiteral(jsNameOf(sym))) - else - js.JSDotSelect(genQual, encodeFieldSym(sym)) - fromAny(boxed, - enteringPhase(currentRun.posterasurePhase)(sym.tpe)) - } else*/ { - js.Select(genExpr(qualifier), encodeClassName(sym.owner), - encodeFieldSym(sym))(toIRType(sym.info)) + } else { + val (field, boxed) = genAssignableField(sym, qualifier) + if (boxed) unbox(field, atPhase(elimErasedValueTypePhase)(sym.info)) + else field } case tree: Ident => @@ -1258,8 +1412,6 @@ class JSCodeGen()(using genCtx: Context) { /*if (!sym.is(Mutable) && !ctorAssignment) throw new FatalError(s"Assigning to immutable field ${sym.fullName} at $pos")*/ - val genQual = genExpr(qualifier) - if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) { /* This is an assignment to a @js.native field. Since we reject * `@js.native var`s as compile errors, this can only happen in @@ -1268,21 +1420,16 @@ class JSCodeGen()(using genCtx: Context) { * emitted at all. */ js.Skip() - } else /*if (isScalaJSDefinedJSClass(sym.owner)) { - val genLhs = if (isExposed(sym)) - js.JSBracketSelect(genQual, js.StringLiteral(jsNameOf(sym))) - else - js.JSDotSelect(genQual, encodeFieldSym(sym)) - val boxedRhs = - ensureBoxed(genRhs, - enteringPhase(currentRun.posterasurePhase)(rhs.tpe)) - js.Assign(genLhs, boxedRhs) - } else*/ { - js.Assign( - js.Select(genQual, encodeClassName(sym.owner), - encodeFieldSym(sym))(toIRType(sym.info)), - genRhs) + } else { + val (field, boxed) = genAssignableField(sym, qualifier) + if (boxed) { + val genBoxedRhs = box(genRhs, atPhase(elimErasedValueTypePhase)(sym.info)) + js.Assign(field, genBoxedRhs) + } else { + js.Assign(field,genRhs) + } } + case _ => js.Assign( js.VarRef(encodeLocalSym(sym))(toIRType(sym.info)), @@ -1511,9 +1658,9 @@ class JSCodeGen()(using genCtx: Context) { if (sym == defn.Any_getClass) { // The only primitive that is also callable as super call js.GetClass(genThis()) - } else /*if (isScalaJSDefinedJSClass(currentClassSym)) { + } else if (currentClassSym.isNonNativeJSClass) { genJSSuperCall(tree, isStat) - } else*/ { + } else { /* #3013 `qual` can be `this.$outer()` in some cases since Scala 2.12, * so we call `genExpr(qual)`, not just `genThis()`. */ @@ -1561,9 +1708,7 @@ class JSCodeGen()(using genCtx: Context) { val functionMaker = translatedAnonFunctions(tpe.typeSymbol) functionMaker(args map genExpr) } else*/ if (isJSType(clsSym)) { - if (clsSym == jsdefn.JSObjectClass && args.isEmpty) js.JSObjectConstr(Nil) - else if (clsSym == jsdefn.JSArrayClass && args.isEmpty) js.JSArrayConstr(Nil) - else js.JSNew(genLoadJSConstructor(clsSym), genActualJSArgs(ctor, args)) + genNewJSClass(tree) } else { toTypeRef(tpe) match { case jstpe.ClassRef(className) => @@ -1592,6 +1737,43 @@ class JSCodeGen()(using genCtx: Context) { jstpe.ClassType(className)) } + /** Gen JS code for a new of a JS class (subclass of `js.Any`). */ + private def genNewJSClass(tree: Apply): js.Tree = { + acquireContextualJSClassValue { jsClassValue => + implicit val pos: Position = tree.span + + val Apply(fun @ Select(New(tpt), _), args) = tree + val cls = tpt.tpe.typeSymbol + val ctor = fun.symbol + + val nestedJSClass = cls.isNestedJSClass + assert(jsClassValue.isDefined == nestedJSClass, + s"$cls at $pos: jsClassValue.isDefined = ${jsClassValue.isDefined} " + + s"but isInnerNonNativeJSClass = $nestedJSClass") + + def genArgs: List[js.TreeOrJSSpread] = genActualJSArgs(ctor, args) + + if (cls == jsdefn.JSObjectClass && args.isEmpty) + js.JSObjectConstr(Nil) + else if (cls == jsdefn.JSArrayClass && args.isEmpty) + js.JSArrayConstr(Nil) + //else if (cls.isAnonymousClass) + // genAnonJSClassNew(cls, jsClassValue.get, genArgs)(fun.pos) + else if (!nestedJSClass) + js.JSNew(genLoadJSConstructor(cls), genArgs) + else if (!atPhase(erasurePhase)(cls.is(ModuleClass))) // LambdaLift removes the ModuleClass flag of lifted classes + js.JSNew(jsClassValue.get, genArgs) + else + genCreateInnerJSModule(cls, jsClassValue.get, args.map(genExpr)) + } + } + + /** Gen JS code to create the JS class of an inner JS module class. */ + private def genCreateInnerJSModule(sym: Symbol, jsSuperClassValue: js.Tree, args: List[js.Tree])( + implicit pos: Position): js.Tree = { + js.JSNew(js.CreateJSClass(encodeClassName(sym), jsSuperClassValue :: args), Nil) + } + /** Gen JS code for a primitive method call. */ private def genPrimitiveOp(tree: Apply, isStat: Boolean): js.Tree = { import dotty.tools.backend.ScalaPrimitivesOps._ @@ -2136,10 +2318,10 @@ class JSCodeGen()(using genCtx: Context) { if (isMethodStaticInIR(sym)) { genApplyStatic(sym, genActualArgs(sym, args)) } else if (isJSType(sym.owner)) { - //if (!isScalaJSDefinedJSClass(sym.owner) || isExposed(sym)) + if (!sym.owner.isNonNativeJSClass || sym.isJSExposed) genApplyJSMethodGeneric(sym, genExprOrGlobalScope(receiver), genActualJSArgs(sym, args), isStat)(tree.sourcePos) - /*else - genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args))*/ + else + genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args)) } else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) { genJSNativeMemberCall(tree, isStat) } else { @@ -2328,6 +2510,37 @@ class JSCodeGen()(using genCtx: Context) { }) } + private def genJSSuperCall(tree: Apply, isStat: Boolean): js.Tree = { + acquireContextualJSClassValue { explicitJSSuperClassValue => + implicit val pos = tree.span + val Apply(fun @ Select(sup @ Super(qual, _), _), args) = tree + val sym = fun.symbol + + val genReceiver = genExpr(qual) + def genScalaArgs = genActualArgs(sym, args) + def genJSArgs = genActualJSArgs(sym, args) + + if (sym.owner == defn.ObjectClass) { + // Normal call anyway + assert(!sym.isClassConstructor, + s"Trying to call the super constructor of Object in a non-native JS class at $pos") + genApplyMethod(genReceiver, sym, genScalaArgs) + } else if (sym.isClassConstructor) { + assert(genReceiver.isInstanceOf[js.This], + s"Trying to call a JS super constructor with a non-`this` receiver at $pos") + js.JSSuperConstructorCall(genJSArgs) + } else if (sym.owner.isNonNativeJSClass && !sym.isJSExposed) { + // Reroute to the static method + genApplyJSClassMethod(genReceiver, sym, genScalaArgs) + } else { + val jsSuperClassValue = explicitJSSuperClassValue.orElse { + Some(genLoadJSConstructor(currentClassSym.get.asClass.superClass)) + } + genApplyJSMethodGeneric(sym, MaybeGlobalScope.NotGlobalScope(genReceiver), + genJSArgs, isStat, jsSuperClassValue)(tree.sourcePos) + } + } + } /** Gen JS code for a call to a polymorphic method. * @@ -2588,8 +2801,10 @@ class JSCodeGen()(using genCtx: Context) { genApplyStatic(sym, formalCaptures.map(_.ref) ::: actualParams) } else { val thisCaptureRef :: argCaptureRefs = formalCaptures.map(_.ref) - genApplyMethodMaybeStatically(thisCaptureRef, sym, - argCaptureRefs ::: actualParams) + if (!sym.owner.isNonNativeJSClass || sym.isJSExposed) + genApplyMethodMaybeStatically(thisCaptureRef, sym, argCaptureRefs ::: actualParams) + else + genApplyJSClassMethod(thisCaptureRef, sym, argCaptureRefs ::: actualParams) } box(call, sym.info.finalResultType) } @@ -2635,9 +2850,7 @@ class JSCodeGen()(using genCtx: Context) { * @param tpeEnteringElimErasedValueType The type of `expr` as it was * entering the `elimErasedValueType` phase. */ - private def box(expr: js.Tree, tpeEnteringElimErasedValueType: Type)( - implicit pos: Position): js.Tree = { - + def box(expr: js.Tree, tpeEnteringElimErasedValueType: Type)(implicit pos: Position): js.Tree = { tpeEnteringElimErasedValueType match { case tpe if isPrimitiveValueType(tpe) => makePrimitiveBox(expr, tpe) @@ -2662,9 +2875,7 @@ class JSCodeGen()(using genCtx: Context) { * @param tpeEnteringElimErasedValueType The type of `expr` as it was * entering the `elimErasedValueType` phase. */ - private def unbox(expr: js.Tree, tpeEnteringElimErasedValueType: Type)( - implicit pos: Position): js.Tree = { - + def unbox(expr: js.Tree, tpeEnteringElimErasedValueType: Type)(implicit pos: Position): js.Tree = { tpeEnteringElimErasedValueType match { case tpe if isPrimitiveValueType(tpe) => makePrimitiveUnbox(expr, tpe) @@ -2714,7 +2925,7 @@ class JSCodeGen()(using genCtx: Context) { } /** Gen JS code for an isInstanceOf test (for reference types only) */ - private def genIsInstanceOf(value: js.Tree, to: Type)( + def genIsInstanceOf(value: js.Tree, to: Type)( implicit pos: SourcePosition): js.Tree = { val sym = to.typeSymbol @@ -2749,8 +2960,7 @@ class JSCodeGen()(using genCtx: Context) { } /** Gen a dynamically linked call to a Scala method. */ - private def genApplyMethod(receiver: js.Tree, method: Symbol, - arguments: List[js.Tree])( + def genApplyMethod(receiver: js.Tree, method: Symbol, arguments: List[js.Tree])( implicit pos: Position): js.Tree = { assert(!method.isPrivate, s"Cannot generate a dynamic call to private method $method at $pos") @@ -2759,8 +2969,8 @@ class JSCodeGen()(using genCtx: Context) { } /** Gen a statically linked call to an instance method. */ - private def genApplyMethodStatically(receiver: js.Tree, method: Symbol, - arguments: List[js.Tree])(implicit pos: Position): js.Tree = { + def genApplyMethodStatically(receiver: js.Tree, method: Symbol, arguments: List[js.Tree])( + implicit pos: Position): js.Tree = { val flags = js.ApplyFlags.empty .withPrivate(method.isPrivate && !method.isClassConstructor) .withConstructor(method.isClassConstructor) @@ -2778,8 +2988,8 @@ class JSCodeGen()(using genCtx: Context) { } /** Gen a call to a non-exposed method of a non-native JS class. */ - private def genApplyJSClassMethod(receiver: js.Tree, method: Symbol, - arguments: List[js.Tree])(implicit pos: Position): js.Tree = { + def genApplyJSClassMethod(receiver: js.Tree, method: Symbol, arguments: List[js.Tree])( + implicit pos: Position): js.Tree = { genApplyStatic(method, receiver :: arguments) } @@ -2878,7 +3088,6 @@ class JSCodeGen()(using genCtx: Context) { else genLoadJSConstructor(classSym) - /* case CREATE_INNER_JS_CLASS | CREATE_LOCAL_JS_CLASS => // runtime.createInnerJSClass(clazz, superClass) // runtime.createLocalJSClass(clazz, superClass, fakeNewInstances) @@ -2890,14 +3099,13 @@ class JSCodeGen()(using genCtx: Context) { val captureValues = { if (code == CREATE_INNER_JS_CLASS) { val outer = genThis() - List.fill(classSym.info.decls.count(_.isClassConstructor))(outer) + List.fill(classSym.info.decls.lookupAll(nme.CONSTRUCTOR).size)(outer) } else { - val ArrayValue(_, fakeNewInstances) = args(2) + val fakeNewInstances = args(2).asInstanceOf[JavaSeqLiteral].elems fakeNewInstances.flatMap(genCaptureValuesFromFakeNewInstance(_)) } } - js.CreateJSClass(encodeClassRef(classSym), - superClassValue :: captureValues) + js.CreateJSClass(encodeClassName(classSym), superClassValue :: captureValues) } case WITH_CONTEXTUAL_JS_CLASS_VALUE => @@ -2908,7 +3116,6 @@ class JSCodeGen()(using genCtx: Context) { ) { genStatOrExpr(args(1), isStat) } - */ case LINKING_INFO => // runtime.linkingInfo @@ -2988,6 +3195,22 @@ class JSCodeGen()(using genCtx: Context) { js.JSFunctionApply(fVarDef.ref, List(keyVarRef)) })) + case UNION_FROM | UNION_FROM_TYPE_CONSTRUCTOR => + /* js.|.from and js.|.fromTypeConstructor + * We should not have to deal with those. They have a perfectly valid + * user-space implementation. However, the Dotty type checker inserts + * way too many of those, even when they are completely unnecessary. + * That still wouldn't be an issue ... if only it did not insert them + * around the default getters to their parameters! But even there it + * does it (although the types are, by construction, *equivalent*!), + * and that kills our `UndefinedParam` treatment. So we have to handle + * those two methods as primitives to completely eliminate them. + * + * Hopefully this will become unnecessary when/if we manage to + * reinterpret js.| as a true Dotty union type. + */ + genArgs2._1 + case REFLECT_SELECTABLE_SELECTDYN => // scala.reflect.Selectable.selectDynamic genReflectiveCall(tree, isSelectDynamic = true) @@ -3215,6 +3438,9 @@ class JSCodeGen()(using genCtx: Context) { def paramNamesAndTypes(using Context): List[(Names.TermName, Type)] = sym.info.paramNamess.flatten.zip(sym.info.paramInfoss.flatten) + /*val isAnonJSClassConstructor = + sym.isClassConstructor && sym.owner.isAnonymousClass*/ + val wereRepeated = atPhase(elimRepeatedPhase) { val list = for ((name, tpe) <- paramNamesAndTypes) @@ -3230,20 +3456,30 @@ class JSCodeGen()(using genCtx: Context) { val argsParamNamesAndTypes = args.zip(paramNamesAndTypes) for ((arg, (paramName, paramType)) <- argsParamNamesAndTypes) { - val wasRepeated = wereRepeated.getOrElse(paramName, false) - if (wasRepeated) { - reversedArgs = - genJSRepeatedParam(arg) reverse_::: reversedArgs - } else { - val unboxedArg = genExpr(arg) - val boxedArg = unboxedArg match { - case js.Transient(UndefinedParam) => - unboxedArg - case _ => - val tpe = paramTypes.getOrElse(paramName, paramType) - box(unboxedArg, tpe) - } - reversedArgs ::= boxedArg + val wasRepeated = + /*if (isAnonJSClassConstructor) Some(false) + else*/ wereRepeated.get(paramName) + + wasRepeated match { + case Some(true) => + reversedArgs = genJSRepeatedParam(arg) reverse_::: reversedArgs + + case Some(false) => + val unboxedArg = genExpr(arg) + val boxedArg = unboxedArg match { + case js.Transient(UndefinedParam) => + unboxedArg + case _ => + val tpe = paramTypes.getOrElse(paramName, paramType) + box(unboxedArg, tpe) + } + reversedArgs ::= boxedArg + + case None => + // This is a parameter introduced by erasure or lambdalift, which we ignore. + assert(sym.isClassConstructor, + i"Found an unknown param $paramName in method " + + i"${sym.fullName}, which is not a class constructor, at $pos") } } @@ -3349,6 +3585,69 @@ class JSCodeGen()(using genCtx: Context) { } } + /** Wraps a `js.Array` to use as varargs. */ + def genJSArrayToVarArgs(arrayRef: js.Tree)(implicit pos: SourcePosition): js.Tree = + genModuleApplyMethod(jsdefn.Runtime_toScalaVarArgs, List(arrayRef)) + + /** Gen the actual capture values for a JS constructor based on its fake `new` invocation. */ + private def genCaptureValuesFromFakeNewInstance(tree: Tree): List[js.Tree] = { + implicit val pos: Position = tree.span + + val Apply(fun @ Select(New(_), _), args) = tree + val sym = fun.symbol + + /* We use the same strategy as genActualJSArgs to detect which parameters were + * introduced by explicitouter or lambdalift (but reversed, of course). + */ + + val existedBeforeUncurry = atPhase(elimRepeatedPhase) { + sym.info.paramNamess.flatten.toSet + } + + for { + (arg, paramName) <- args.zip(sym.info.paramNamess.flatten) + if !existedBeforeUncurry(paramName) + } yield { + genExpr(arg) + } + } + + private def genAssignableField(sym: Symbol, qualifier: Tree)(implicit pos: SourcePosition): (js.Tree, Boolean) = { + def qual = genExpr(qualifier) + + if (sym.owner.isNonNativeJSClass) { + val f = if (sym.isJSExposed) { + js.JSSelect(qual, genExpr(sym.jsName)) + } else /*if (sym.owner.isAnonymousClass) { + js.JSSelect( + js.JSSelect(qual, genPrivateFieldsSymbol()), + encodeFieldSymAsStringLiteral(sym)) + } else*/ { + js.JSPrivateSelect(qual, encodeClassName(sym.owner), + encodeFieldSym(sym)) + } + + (f, true) + } else /*if (jsInterop.topLevelExportsOf(sym).nonEmpty) { + val f = js.SelectStatic(encodeClassName(sym.owner), + encodeFieldSym(sym))(jstpe.AnyType) + (f, true) + } else if (jsInterop.staticExportsOf(sym).nonEmpty) { + val exportInfo = jsInterop.staticExportsOf(sym).head + val companionClass = patchedLinkedClassOfClass(sym.owner) + val f = js.JSSelect( + genLoadJSConstructor(companionClass), + js.StringLiteral(exportInfo.jsName)) + + (f, true) + } else*/ { + val f = js.Select(qual, encodeClassName(sym.owner), + encodeFieldSym(sym))(toIRType(sym.info)) + + (f, false) + } + } + /** Gen JS code for loading a Java static field. */ private def genLoadStaticField(sym: Symbol)(implicit pos: SourcePosition): js.Tree = { @@ -3378,7 +3677,7 @@ class JSCodeGen()(using genCtx: Context) { * that a global scope object should only be used as the qualifier of a * `.`-selection. */ - private def genLoadModule(sym: Symbol)(implicit pos: SourcePosition): js.Tree = + def genLoadModule(sym: Symbol)(implicit pos: SourcePosition): js.Tree = ruleOutGlobalScope(genLoadModuleOrGlobalScope(sym)) /** Generate loading of a module value or the global scope. @@ -3411,7 +3710,7 @@ class JSCodeGen()(using genCtx: Context) { private def genLoadJSConstructor(sym: Symbol)( implicit pos: Position): js.Tree = { assert(!isStaticModule(sym) && !sym.is(Trait), - s"genPrimitiveJSClass called with non-class $sym") + s"genLoadJSConstructor called with non-class $sym") js.LoadJSConstructor(encodeClassName(sym)) } @@ -3661,6 +3960,15 @@ class JSCodeGen()(using genCtx: Context) { private def isMaybeJavaScriptException(tpe: Type): Boolean = jsdefn.JavaScriptExceptionClass.isSubClass(tpe.typeSymbol) + private def hasDefaultCtorArgsAndJSModule(classSym: Symbol): Boolean = { + def hasNativeCompanion = + classSym.companionModule.moduleClass.hasAnnotation(jsdefn.JSNativeAnnot) + def hasDefaultParameters = + classSym.info.decls.exists(sym => sym.isClassConstructor && sym.hasDefaultParams) + + hasNativeCompanion && hasDefaultParameters + } + // Copied from DottyBackendInterface private val desugared = new java.util.IdentityHashMap[Type, tpd.Select] diff --git a/compiler/src/dotty/tools/backend/sjs/JSConstructorGen.scala b/compiler/src/dotty/tools/backend/sjs/JSConstructorGen.scala new file mode 100644 index 000000000000..da4db4760551 --- /dev/null +++ b/compiler/src/dotty/tools/backend/sjs/JSConstructorGen.scala @@ -0,0 +1,376 @@ +package dotty.tools.backend.sjs + +import org.scalajs.ir +import org.scalajs.ir.{Position, Trees => js, Types => jstpe} +import org.scalajs.ir.Names._ +import org.scalajs.ir.OriginalName.NoOriginalName + +import JSCodeGen.UndefinedParam + +object JSConstructorGen { + + /** Builds one JS constructor out of several "init" methods and their + * dispatcher. + * + * This method and the rest of this file are copied verbatim from `GenJSCode` + * for scalac, since there is no dependency on the compiler trees/symbols/etc. + * We are only manipulating IR trees and types. + * + * The only difference is the two parameters `overloadIdent` and `reportError`, + * which are added so that this entire file can be even more isolated. + */ + def buildJSConstructorDef(dispatch: js.JSMethodDef, ctors: List[js.MethodDef], + overloadIdent: js.LocalIdent)( + reportError: String => Unit)( + implicit pos: Position): js.JSMethodDef = { + + val js.JSMethodDef(_, dispatchName, dispatchArgs, dispatchResolution) = + dispatch + + val jsConstructorBuilder = mkJSConstructorBuilder(ctors, reportError) + + // Section containing the overload resolution and casts of parameters + val overloadSelection = mkOverloadSelection(jsConstructorBuilder, + overloadIdent, dispatchResolution) + + /* Section containing all the code executed before the call to `this` + * for every secondary constructor. + */ + val prePrimaryCtorBody = + jsConstructorBuilder.mkPrePrimaryCtorBody(overloadIdent) + + val primaryCtorBody = jsConstructorBuilder.primaryCtorBody + + /* Section containing all the code executed after the call to this for + * every secondary constructor. + */ + val postPrimaryCtorBody = + jsConstructorBuilder.mkPostPrimaryCtorBody(overloadIdent) + + val newBody = js.Block(overloadSelection ::: prePrimaryCtorBody :: + primaryCtorBody :: postPrimaryCtorBody :: js.Undefined() :: Nil) + + js.JSMethodDef(js.MemberFlags.empty, dispatchName, dispatchArgs, newBody)( + dispatch.optimizerHints, None) + } + + private class ConstructorTree(val overrideNum: Int, val method: js.MethodDef, + val subConstructors: List[ConstructorTree]) { + + lazy val overrideNumBounds: (Int, Int) = + if (subConstructors.isEmpty) (overrideNum, overrideNum) + else (subConstructors.head.overrideNumBounds._1, overrideNum) + + def get(methodName: MethodName): Option[ConstructorTree] = { + if (methodName == this.method.methodName) { + Some(this) + } else { + subConstructors.iterator.map(_.get(methodName)).collectFirst { + case Some(node) => node + } + } + } + + def getParamRefs(implicit pos: Position): List[js.VarRef] = + method.args.map(_.ref) + + def getAllParamDefsAsVars(implicit pos: Position): List[js.VarDef] = { + val localDefs = method.args.map { pDef => + js.VarDef(pDef.name, pDef.originalName, pDef.ptpe, mutable = true, + jstpe.zeroOf(pDef.ptpe)) + } + localDefs ++ subConstructors.flatMap(_.getAllParamDefsAsVars) + } + } + + private class JSConstructorBuilder(root: ConstructorTree, reportError: String => Unit) { + + def primaryCtorBody: js.Tree = root.method.body.getOrElse( + throw new AssertionError("Found abstract constructor")) + + def hasSubConstructors: Boolean = root.subConstructors.nonEmpty + + def getOverrideNum(methodName: MethodName): Int = + root.get(methodName).fold(-1)(_.overrideNum) + + def getParamRefsFor(methodName: MethodName)(implicit pos: Position): List[js.VarRef] = + root.get(methodName).fold(List.empty[js.VarRef])(_.getParamRefs) + + def getAllParamDefsAsVars(implicit pos: Position): List[js.VarDef] = + root.getAllParamDefsAsVars + + def mkPrePrimaryCtorBody(overrideNumIdent: js.LocalIdent)( + implicit pos: Position): js.Tree = { + val overrideNumRef = js.VarRef(overrideNumIdent)(jstpe.IntType) + mkSubPreCalls(root, overrideNumRef) + } + + def mkPostPrimaryCtorBody(overrideNumIdent: js.LocalIdent)( + implicit pos: Position): js.Tree = { + val overrideNumRef = js.VarRef(overrideNumIdent)(jstpe.IntType) + js.Block(mkSubPostCalls(root, overrideNumRef)) + } + + private def mkSubPreCalls(constructorTree: ConstructorTree, + overrideNumRef: js.VarRef)(implicit pos: Position): js.Tree = { + val overrideNumss = constructorTree.subConstructors.map(_.overrideNumBounds) + val paramRefs = constructorTree.getParamRefs + val bodies = constructorTree.subConstructors.map { constructorTree => + mkPrePrimaryCtorBodyOnSndCtr(constructorTree, overrideNumRef, paramRefs) + } + overrideNumss.zip(bodies).foldRight[js.Tree](js.Skip()) { + case ((numBounds, body), acc) => + val cond = mkOverrideNumsCond(overrideNumRef, numBounds) + js.If(cond, body, acc)(jstpe.BooleanType) + } + } + + private def mkPrePrimaryCtorBodyOnSndCtr(constructorTree: ConstructorTree, + overrideNumRef: js.VarRef, outputParams: List[js.VarRef])( + implicit pos: Position): js.Tree = { + val subCalls = + mkSubPreCalls(constructorTree, overrideNumRef) + + val preSuperCall = { + def checkForUndefinedParams(args: List[js.Tree]): List[js.Tree] = { + def isUndefinedParam(tree: js.Tree): Boolean = tree match { + case js.Transient(UndefinedParam) => true + case _ => false + } + + if (!args.exists(isUndefinedParam)) { + args + } else { + /* If we find an undefined param here, we're in trouble, because + * the handling of a default param for the target constructor has + * already been done during overload resolution. If we store an + * `undefined` now, it will fall through without being properly + * processed. + * + * Since this seems very tricky to deal with, and a pretty rare + * use case (with a workaround), we emit an "implementation + * restriction" error. + */ + reportError( + "Implementation restriction: in a JS class, a secondary " + + "constructor calling another constructor with default " + + "parameters must provide the values of all parameters.") + + /* Replace undefined params by undefined to prevent subsequent + * compiler crashes. + */ + args.map { arg => + if (isUndefinedParam(arg)) + js.Undefined()(arg.pos) + else + arg + } + } + } + + constructorTree.method.body.get match { + case js.Block(stats) => + val beforeSuperCall = stats.takeWhile { + case js.ApplyStatic(_, _, mtd, _) => !mtd.name.isConstructor + case _ => true + } + val superCallParams = stats.collectFirst { + case js.ApplyStatic(_, _, mtd, js.This() :: args) + if mtd.name.isConstructor => + val checkedArgs = checkForUndefinedParams(args) + zipMap(outputParams, checkedArgs)(js.Assign(_, _)) + }.getOrElse(Nil) + + beforeSuperCall ::: superCallParams + + case js.ApplyStatic(_, _, mtd, js.This() :: args) + if mtd.name.isConstructor => + val checkedArgs = checkForUndefinedParams(args) + zipMap(outputParams, checkedArgs)(js.Assign(_, _)) + + case _ => Nil + } + } + + js.Block(subCalls :: preSuperCall) + } + + private def mkSubPostCalls(constructorTree: ConstructorTree, + overrideNumRef: js.VarRef)(implicit pos: Position): js.Tree = { + val overrideNumss = constructorTree.subConstructors.map(_.overrideNumBounds) + val bodies = constructorTree.subConstructors.map { ct => + mkPostPrimaryCtorBodyOnSndCtr(ct, overrideNumRef) + } + overrideNumss.zip(bodies).foldRight[js.Tree](js.Skip()) { + case ((numBounds, js.Skip()), acc) => acc + + case ((numBounds, body), acc) => + val cond = mkOverrideNumsCond(overrideNumRef, numBounds) + js.If(cond, body, acc)(jstpe.BooleanType) + } + } + + private def mkPostPrimaryCtorBodyOnSndCtr(constructorTree: ConstructorTree, + overrideNumRef: js.VarRef)(implicit pos: Position): js.Tree = { + val postSuperCall = { + constructorTree.method.body.get match { + case js.Block(stats) => + stats.dropWhile { + case js.ApplyStatic(_, _, mtd, _) => !mtd.name.isConstructor + case _ => true + }.tail + + case _ => Nil + } + } + js.Block(postSuperCall :+ mkSubPostCalls(constructorTree, overrideNumRef)) + } + + private def mkOverrideNumsCond(numRef: js.VarRef, + numBounds: (Int, Int))(implicit pos: Position) = numBounds match { + case (lo, hi) if lo == hi => + js.BinaryOp(js.BinaryOp.Int_==, js.IntLiteral(lo), numRef) + + case (lo, hi) if lo == hi - 1 => + val lhs = js.BinaryOp(js.BinaryOp.Int_==, numRef, js.IntLiteral(lo)) + val rhs = js.BinaryOp(js.BinaryOp.Int_==, numRef, js.IntLiteral(hi)) + js.If(lhs, js.BooleanLiteral(true), rhs)(jstpe.BooleanType) + + case (lo, hi) => + val lhs = js.BinaryOp(js.BinaryOp.Int_<=, js.IntLiteral(lo), numRef) + val rhs = js.BinaryOp(js.BinaryOp.Int_<=, numRef, js.IntLiteral(hi)) + js.BinaryOp(js.BinaryOp.Boolean_&, lhs, rhs) + js.If(lhs, rhs, js.BooleanLiteral(false))(jstpe.BooleanType) + } + } + + private def zipMap[T, U, V](xs: List[T], ys: List[U])( + f: (T, U) => V): List[V] = { + for ((x, y) <- xs zip ys) yield f(x, y) + } + + /** mkOverloadSelection return a list of `stats` with that starts with: + * 1) The definition for the local variable that will hold the overload + * resolution number. + * 2) The definitions of all local variables that are used as parameters + * in all the constructors. + * 3) The overload resolution match/if statements. For each overload the + * overload number is assigned and the parameters are cast and assigned + * to their corresponding variables. + */ + private def mkOverloadSelection(jsConstructorBuilder: JSConstructorBuilder, + overloadIdent: js.LocalIdent, dispatchResolution: js.Tree)( + implicit pos: Position): List[js.Tree] = { + + def deconstructApplyCtor(body: js.Tree): (List[js.Tree], MethodName, List[js.Tree]) = { + val (prepStats, applyCtor) = (body: @unchecked) match { + case applyCtor: js.ApplyStatic => + (Nil, applyCtor) + case js.Block(prepStats :+ (applyCtor: js.ApplyStatic)) => + (prepStats, applyCtor) + } + val js.ApplyStatic(_, _, js.MethodIdent(ctorName), js.This() :: ctorArgs) = + applyCtor + assert(ctorName.isConstructor, + s"unexpected super constructor call to non-constructor $ctorName at ${applyCtor.pos}") + (prepStats, ctorName, ctorArgs) + } + + if (!jsConstructorBuilder.hasSubConstructors) { + val (prepStats, ctorName, ctorArgs) = + deconstructApplyCtor(dispatchResolution) + + val refs = jsConstructorBuilder.getParamRefsFor(ctorName) + assert(refs.size == ctorArgs.size, s"at $pos") + val assignCtorParams = zipMap(refs, ctorArgs) { (ref, ctorArg) => + js.VarDef(ref.ident, NoOriginalName, ref.tpe, mutable = false, ctorArg) + } + + prepStats ::: assignCtorParams + } else { + val overloadRef = js.VarRef(overloadIdent)(jstpe.IntType) + + /* transformDispatch takes the body of the method generated by + * `genJSConstructorDispatch` and transform it recursively. + */ + def transformDispatch(tree: js.Tree): js.Tree = tree match { + // Parameter count resolution + case js.Match(selector, cases, default) => + val newCases = cases.map { + case (literals, body) => (literals, transformDispatch(body)) + } + val newDefault = transformDispatch(default) + js.Match(selector, newCases, newDefault)(tree.tpe) + + // Parameter type resolution + case js.If(cond, thenp, elsep) => + js.If(cond, transformDispatch(thenp), + transformDispatch(elsep))(tree.tpe) + + // Throw(StringLiteral(No matching overload)) + case tree: js.Throw => + tree + + // Overload resolution done, apply the constructor + case _ => + val (prepStats, ctorName, ctorArgs) = deconstructApplyCtor(tree) + + val num = jsConstructorBuilder.getOverrideNum(ctorName) + val overloadAssign = js.Assign(overloadRef, js.IntLiteral(num)) + + val refs = jsConstructorBuilder.getParamRefsFor(ctorName) + assert(refs.size == ctorArgs.size, s"at $pos") + val assignCtorParams = zipMap(refs, ctorArgs)(js.Assign(_, _)) + + js.Block(overloadAssign :: prepStats ::: assignCtorParams) + } + + val newDispatchResolution = transformDispatch(dispatchResolution) + val allParamDefsAsVars = jsConstructorBuilder.getAllParamDefsAsVars + val overrideNumDef = js.VarDef(overloadIdent, NoOriginalName, + jstpe.IntType, mutable = true, js.IntLiteral(0)) + + overrideNumDef :: allParamDefsAsVars ::: newDispatchResolution :: Nil + } + } + + private def mkJSConstructorBuilder(ctors: List[js.MethodDef], reportError: String => Unit)( + implicit pos: Position): JSConstructorBuilder = { + def findCtorForwarderCall(tree: js.Tree): MethodName = (tree: @unchecked) match { + case js.ApplyStatic(_, _, method, js.This() :: _) + if method.name.isConstructor => + method.name + + case js.Block(stats) => + stats.collectFirst { + case js.ApplyStatic(_, _, method, js.This() :: _) + if method.name.isConstructor => + method.name + }.get + } + + val (primaryCtor :: Nil, secondaryCtors) = ctors.partition { + _.body.get match { + case js.Block(stats) => + stats.exists(_.isInstanceOf[js.JSSuperConstructorCall]) + + case _: js.JSSuperConstructorCall => true + case _ => false + } + } + + val ctorToChildren = secondaryCtors.map { ctor => + findCtorForwarderCall(ctor.body.get) -> ctor + }.groupBy(_._1).map(kv => kv._1 -> kv._2.map(_._2)).withDefaultValue(Nil) + + var overrideNum = -1 + def mkConstructorTree(method: js.MethodDef): ConstructorTree = { + val subCtrTrees = ctorToChildren(method.methodName).map(mkConstructorTree) + overrideNum += 1 + new ConstructorTree(overrideNum, method, subCtrTrees) + } + + new JSConstructorBuilder(mkConstructorTree(primaryCtor), reportError: String => Unit) + } + +} diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index 17acfd83cac9..dd0471921a4c 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -54,11 +54,15 @@ final class JSDefinitions()(using Context) { @threadUnsafe lazy val PseudoUnionModuleRef = requiredModuleRef("scala.scalajs.js.|") def PseudoUnionModule(using Context) = PseudoUnionModuleRef.symbol + @threadUnsafe lazy val PseudoUnion_fromR = PseudoUnionModule.requiredMethodRef("from") + def PseudoUnion_from(using Context) = PseudoUnion_fromR.symbol @threadUnsafe lazy val PseudoUnion_fromTypeConstructorR = PseudoUnionModule.requiredMethodRef("fromTypeConstructor") def PseudoUnion_fromTypeConstructor(using Context) = PseudoUnion_fromTypeConstructorR.symbol @threadUnsafe lazy val JSArrayType: TypeRef = requiredClassRef("scala.scalajs.js.Array") def JSArrayClass(using Context) = JSArrayType.symbol.asClass + @threadUnsafe lazy val JSDynamicType: TypeRef = requiredClassRef("scala.scalajs.js.Dynamic") + def JSDynamicClass(using Context) = JSDynamicType.symbol.asClass @threadUnsafe lazy val JSFunctionType = (0 to 22).map(n => requiredClassRef("scala.scalajs.js.Function" + n)).toArray def JSFunctionClass(n: Int)(using Context) = JSFunctionType(n).symbol.asClass @@ -153,6 +157,12 @@ final class JSDefinitions()(using Context) { def Runtime_constructorOf(using Context) = Runtime_constructorOfR.symbol @threadUnsafe lazy val Runtime_newConstructorTagR = RuntimePackageClass.requiredMethodRef("newConstructorTag") def Runtime_newConstructorTag(using Context) = Runtime_newConstructorTagR.symbol + @threadUnsafe lazy val Runtime_createInnerJSClassR = RuntimePackageClass.requiredMethodRef("createInnerJSClass") + def Runtime_createInnerJSClass(using Context) = Runtime_createInnerJSClassR.symbol + @threadUnsafe lazy val Runtime_createLocalJSClassR = RuntimePackageClass.requiredMethodRef("createLocalJSClass") + def Runtime_createLocalJSClass(using Context) = Runtime_createLocalJSClassR.symbol + @threadUnsafe lazy val Runtime_withContextualJSClassValueR = RuntimePackageClass.requiredMethodRef("withContextualJSClassValue") + def Runtime_withContextualJSClassValue(using Context) = Runtime_withContextualJSClassValueR.symbol @threadUnsafe lazy val Runtime_linkingInfoR = RuntimePackageClass.requiredMethodRef("linkingInfo") def Runtime_linkingInfo(using Context) = Runtime_linkingInfoR.symbol diff --git a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala index b0256ffe9ea7..8efa0d35af31 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala @@ -43,6 +43,22 @@ import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions */ object JSEncoding { + /** Name of the capture param storing the JS super class. + * + * This is used by the dispatchers of exposed JS methods and properties of + * nested JS classes when they need to perform a super call. Other super + * calls (in the actual bodies of the methods, not in the dispatchers) do + * not use this value, since they are implemented as static methods that do + * not have access to it. Instead, they get the JS super class value through + * the magic method inserted by `ExplicitLocalJS`, leveraging `lambdalift` + * to ensure that it is properly captured. + * + * Using this identifier is only allowed if it was reserved in the current + * local name scope using [[reserveLocalName]]. Otherwise, this name can + * clash with another local identifier. + */ + final val JSSuperClassParamName = LocalName("superClass$") + private val ScalaRuntimeNothingClassName = ClassName("scala.runtime.Nothing$") private val ScalaRuntimeNullClassName = ClassName("scala.runtime.Null$") @@ -57,6 +73,12 @@ object JSEncoding { private val labelSymbolNames = mutable.Map.empty[Symbol, LabelName] private var returnLabelName: Option[LabelName] = None + def reserveLocalName(name: LocalName): Unit = { + require(usedLocalNames.isEmpty, + s"Trying to reserve the name '$name' but names have already been allocated") + usedLocalNames += name + } + private def freshNameGeneric[N <: ir.Names.Name](base: N, usedNamesSet: mutable.Set[N])( withSuffix: (N, String) => N): N = { diff --git a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala new file mode 100644 index 000000000000..baae107aa09a --- /dev/null +++ b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala @@ -0,0 +1,914 @@ +package dotty.tools.backend.sjs + +import scala.annotation.tailrec + +import scala.collection.mutable + +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.core._ + +import Contexts._ +import Decorators._ +import Denotations._ +import Flags._ +import Names._ +import NameKinds.DefaultGetterName +import Periods._ +import Phases._ +import StdNames._ +import Symbols._ +import SymDenotations._ +import Types._ +import TypeErasure.ErasedValueType + +import dotty.tools.dotc.transform.Erasure +import dotty.tools.dotc.util.SourcePosition +import dotty.tools.dotc.util.Spans.Span +import dotty.tools.dotc.report + +import org.scalajs.ir +import org.scalajs.ir.{ClassKind, Position, Names => jsNames, Trees => js, Types => jstpe} +import org.scalajs.ir.Names.{ClassName, MethodName, SimpleMethodName} +import org.scalajs.ir.OriginalName +import org.scalajs.ir.OriginalName.NoOriginalName +import org.scalajs.ir.Position.NoPosition +import org.scalajs.ir.Trees.OptimizerHints + +import dotty.tools.dotc.transform.sjs.JSSymUtils._ + +import JSEncoding._ + +final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { + import jsCodeGen._ + import positionConversions._ + + def genJSClassDispatchers(classSym: Symbol, dispatchMethodsNames: List[JSName]): List[js.MemberDef] = { + dispatchMethodsNames.map(genJSClassDispatcher(classSym, _)) + } + + private def genJSClassDispatcher(classSym: Symbol, name: JSName): js.MemberDef = { + val alts = classSym.info.membersBasedOnFlags(required = Method, excluded = Bridge) + .map(_.symbol) + .filter { sym => + /* scala-js#3939: Object is not a "real" superclass of JS types. + * as such, its methods do not participate in overload resolution. + * An exception is toString, which is handled specially in genExportMethod. + */ + sym.owner != defn.ObjectClass && sym.jsName == name + } + .toList + + assert(!alts.isEmpty, s"Ended up with no alternatives for ${classSym.fullName}::$name.") + + val (propSyms, methodSyms) = alts.partition(_.isJSProperty) + val isProp = propSyms.nonEmpty + + if (isProp && methodSyms.nonEmpty) { + val firstAlt = alts.head + report.error( + i"Conflicting properties and methods for ${classSym.fullName}::$name.", + firstAlt.srcPos) + implicit val pos = firstAlt.span + js.JSPropertyDef(js.MemberFlags.empty, genExpr(name)(firstAlt.sourcePos), None, None) + } else { + genMemberExportOrDispatcher(name, isProp, alts, static = false) + } + } + + private def genMemberExportOrDispatcher(jsName: JSName, isProp: Boolean, + alts: List[Symbol], static: Boolean): js.MemberDef = { + withNewLocalNameScope { + if (isProp) + genExportProperty(alts, jsName, static) + else + genExportMethod(alts.map(Exported), jsName, static) + } + } + + def genJSConstructorDispatch(alts: List[Symbol]): (Option[List[js.ParamDef]], js.JSMethodDef) = { + val exporteds = alts.map(Exported) + + val isLiftedJSCtor = exporteds.head.isLiftedJSConstructor + assert(exporteds.tail.forall(_.isLiftedJSConstructor == isLiftedJSCtor), + s"Alternative constructors $alts do not agree on whether they are lifted JS constructors or not") + val captureParams = if (!isLiftedJSCtor) { + None + } else { + Some(for { + exported <- exporteds + param <- exported.captureParamsFront ::: exported.captureParamsBack + } yield { + param + }) + } + + val ctorDef = genExportMethod(exporteds, JSName.Literal("constructor"), static = false) + + (captureParams, ctorDef) + } + + private def genExportProperty(alts: List[Symbol], jsName: JSName, static: Boolean): js.JSPropertyDef = { + assert(!alts.isEmpty, s"genExportProperty with empty alternatives for $jsName") + + implicit val pos: Position = alts.head.span + + val namespace = + if (static) js.MemberNamespace.PublicStatic + else js.MemberNamespace.Public + val flags = js.MemberFlags.empty.withNamespace(namespace) + + /* Separate getters and setters. Since we only have getters and setters, we + * simply test the param list size, which is faster than using the full isJSGetter. + */ + val (getter, setters) = alts.partition(_.info.paramInfoss.head.isEmpty) + + // We can have at most one getter + if (getter.sizeIs > 1) { + /* Member export of properties should be caught earlier, so if we get + * here with a non-static export, something went horribly wrong. + */ + assert(static, s"Found more than one instance getter to export for name $jsName.") + for (duplicate <- getter.tail) + report.error(s"Duplicate static getter export with name '${jsName.displayName}'", duplicate) + } + + val getterBody = getter.headOption.map { getterSym => + genApplyForSingleExported(new FormalArgsRegistry(0, false), Exported(getterSym), static) + } + + val setterArgAndBody = { + if (setters.isEmpty) { + None + } else { + val formalArgsRegistry = new FormalArgsRegistry(1, false) + val List(arg) = formalArgsRegistry.genFormalArgs() + val body = genExportSameArgc(jsName, formalArgsRegistry, setters.map(Exported), static, None) + Some((arg, body)) + } + } + + js.JSPropertyDef(flags, genExpr(jsName)(alts.head.sourcePos), getterBody, setterArgAndBody) + } + + private def genExportMethod(alts0: List[Exported], jsName: JSName, static: Boolean): js.JSMethodDef = { + assert(alts0.nonEmpty, "need at least one alternative to generate exporter method") + + implicit val pos = alts0.head.pos + + val namespace = + if (static) js.MemberNamespace.PublicStatic + else js.MemberNamespace.Public + val flags = js.MemberFlags.empty.withNamespace(namespace) + + // toString() is always exported. We might need to add it here to get correct overloading. + val alts = jsName match { + case JSName.Literal("toString") if alts0.forall(_.params.nonEmpty) => + Exported(defn.Any_toString) :: alts0 + case _ => + alts0 + } + + val (formalArgs, body) = + if (alts.tail.isEmpty) genExportMethodSingleAlt(alts.head, static) + else genExportMethodMultiAlts(alts, jsName, static) + + js.JSMethodDef(flags, genExpr(jsName), formalArgs, body)(OptimizerHints.empty, None) + } + + private def genExportMethodSingleAlt(alt: Exported, static: Boolean)( + implicit pos: SourcePosition): (List[js.ParamDef], js.Tree) = { + /* This is a fast path for `genExportMethod` that applies for all methods that + * are not overloaded. In addition to being a fast path, it does a better job + * than `genExportMethodMultiAlts` when the only alternative has default + * parameters, because it avoids a spurious dispatch. + * In scalac, the spurious dispatch was avoided by a more elaborate case + * generation in `genExportMethod`, which was very convoluted and was not + * ported to dotc. + */ + + val params = alt.params + val paramsSize = params.size + + val minArgc = { + // Find the first default param or repeated param + val firstOptionalParamIndex = params.indexWhere(p => p.hasDefault || p.isRepeated) + if (firstOptionalParamIndex == -1) paramsSize + else firstOptionalParamIndex + } + + val hasVarArg = alt.hasRepeatedParam + val maxArgc = if (hasVarArg) paramsSize - 1 else paramsSize + val needsRestParam = maxArgc != minArgc || hasVarArg + val formalArgsRegistry = new FormalArgsRegistry(minArgc, needsRestParam) + + val formalArgs = formalArgsRegistry.genFormalArgs() + val body = genApplyForSingleExported(formalArgsRegistry, alt, static) + + (formalArgs, body) + } + + private def genExportMethodMultiAlts(alts: List[Exported], jsName: JSName, static: Boolean)( + implicit pos: SourcePosition): (List[js.ParamDef], js.Tree) = { + // Factor out methods with variable argument lists. + // They can only be at the end of the lists as enforced by PrepJSExports. + val (varArgMeths, normalMeths) = alts.partition(_.hasRepeatedParam) + + // Highest non-repeated argument count + // For varArgsMeths, we have argc - 1, since a repeated parameter list may also be empty (unlike a normal parameter) + val maxArgc = (varArgMeths.map(_.params.size - 1) ::: normalMeths.map(_.params.size)).max + + // Calculates possible arg counts for normal method + def argCounts(ex: Exported): Seq[Int] = { + val params = ex.params + // Find default param + val dParam = params.indexWhere(_.hasDefault) + if (dParam == -1) Seq(params.size) + else dParam to params.size + } + + // Generate tuples (argc, method) + val methodArgCounts = { + // Normal methods + for { + method <- normalMeths + argc <- argCounts(method) + } yield (argc, method) + } ::: { + // Repeated parameter methods + for { + method <- varArgMeths + argc <- method.params.size - 1 to maxArgc + } yield (argc, method) + } + + // Create the formal args registry + val minArgc = methodArgCounts.minBy(_._1)._1 + val hasVarArg = varArgMeths.nonEmpty + val needsRestParam = maxArgc != minArgc || hasVarArg + val formalArgsRegistry = new FormalArgsRegistry(minArgc, needsRestParam) + + // List of formal parameters + val formalArgs = formalArgsRegistry.genFormalArgs() + + // Create a list of (argCount -> methods), sorted by argCount (methods may appear multiple times) + val methodByArgCount: List[(Int, List[Exported])] = + methodArgCounts.groupMap(_._1)(_._2).toList.sortBy(_._1) // sort for determinism + + // Generate a case block for each (argCount, methods) tuple + // TODO? We could optimize this a bit by putting together all the `argCount`s that have the same methods + // (Scala.js for scalac does that, but the code is very convoluted and it's not clear that it is worth it). + val cases = for { + (argc, methods) <- methodByArgCount + if methods != varArgMeths // exclude default case we're generating anyways for varargs + } yield { + // body of case to disambiguates methods with current count + val caseBody = genExportSameArgc(jsName, formalArgsRegistry, methods, static, Some(argc)) + List(js.IntLiteral(argc - minArgc)) -> caseBody + } + + def defaultCase = { + if (!hasVarArg) + genThrowTypeError() + else + genExportSameArgc(jsName, formalArgsRegistry, varArgMeths, static, None) + } + + val body = { + if (cases.isEmpty) { + defaultCase + } else if (cases.tail.isEmpty && !hasVarArg) { + cases.head._2 + } else { + assert(needsRestParam, "Trying to read rest param length but needsRestParam is false") + val restArgRef = formalArgsRegistry.genRestArgRef() + js.Match( + js.AsInstanceOf(js.JSSelect(restArgRef, js.StringLiteral("length")), jstpe.IntType), + cases.toList, defaultCase)(jstpe.AnyType) + } + } + + (formalArgs, body) + } + + /** Resolves method calls to [[alts]] while assuming they have the same parameter count. + * + * @param jsName + * The JS name of the method, for error reporting + * @param formalArgsRegistry + * The registry of all the formal arguments + * @param alts + * Alternative methods + * @param static + * Whether we are generating a static method + * @param maxArgc + * Maximum number of arguments to use for disambiguation + */ + private def genExportSameArgc(jsName: JSName, formalArgsRegistry: FormalArgsRegistry, + alts: List[Exported], static: Boolean, maxArgc: Option[Int]): js.Tree = { + genExportSameArgcRec(jsName, formalArgsRegistry, alts, paramIndex = 0, static, maxArgc) + } + + /** Resolves method calls to [[alts]] while assuming they have the same parameter count. + * + * @param jsName + * The JS name of the method, for error reporting + * @param formalArgsRegistry + * The registry of all the formal arguments + * @param alts + * Alternative methods + * @param paramIndex + * Index where to start disambiguation (starts at 0, increases through recursion) + * @param static + * Whether we are generating a static method + * @param maxArgc + * Maximum number of arguments to use for disambiguation + */ + private def genExportSameArgcRec(jsName: JSName, formalArgsRegistry: FormalArgsRegistry, alts: List[Exported], + paramIndex: Int, static: Boolean, maxArgc: Option[Int]): js.Tree = { + + implicit val pos = alts.head.pos + + if (alts.sizeIs == 1) { + genApplyForSingleExported(formalArgsRegistry, alts.head, static) + } else if (maxArgc.exists(_ <= paramIndex) || !alts.exists(_.params.size > paramIndex)) { + // We reach here in three cases: + // 1. The parameter list has been exhausted + // 2. The optional argument count restriction has triggered + // 3. We only have (more than once) repeated parameters left + // Therefore, we should fail + reportCannotDisambiguateError(jsName, alts) + js.Undefined() + } else { + val altsByTypeTest = groupByWithoutHashCode(alts) { exported => + typeTestForTpe(exported.exportArgTypeAt(paramIndex)) + } + + if (altsByTypeTest.size == 1) { + // Testing this parameter is not doing any us good + genExportSameArgcRec(jsName, formalArgsRegistry, alts, paramIndex + 1, static, maxArgc) + } else { + // Sort them so that, e.g., isInstanceOf[String] comes before isInstanceOf[Object] + val sortedAltsByTypeTest = topoSortDistinctsBy(altsByTypeTest)(_._1) + + val defaultCase = genThrowTypeError() + + sortedAltsByTypeTest.foldRight[js.Tree](defaultCase) { (elem, elsep) => + val (typeTest, subAlts) = elem + implicit val pos = subAlts.head.pos + + val paramRef = formalArgsRegistry.genArgRef(paramIndex) + val genSubAlts = genExportSameArgcRec(jsName, formalArgsRegistry, + subAlts, paramIndex + 1, static, maxArgc) + + def hasDefaultParam = subAlts.exists { exported => + val params = exported.params + params.size > paramIndex && + params(paramIndex).hasDefault + } + + val optCond = typeTest match { + case PrimitiveTypeTest(tpe, _) => Some(js.IsInstanceOf(paramRef, tpe)) + case InstanceOfTypeTest(tpe) => Some(genIsInstanceOf(paramRef, tpe)) + case NoTypeTest => None + } + + optCond.fold[js.Tree] { + genSubAlts // note: elsep is discarded, obviously + } { cond => + val condOrUndef = if (!hasDefaultParam) cond else { + js.If(cond, js.BooleanLiteral(true), + js.BinaryOp(js.BinaryOp.===, paramRef, js.Undefined()))( + jstpe.BooleanType) + } + js.If(condOrUndef, genSubAlts, elsep)(jstpe.AnyType) + } + } + } + } + } + + private def reportCannotDisambiguateError(jsName: JSName, alts: List[Exported]): Unit = { + val currentClass = currentClassSym.get + + /* Find a position that is in the current class for decent error reporting. + * If there are more than one, always use the "highest" one (i.e., the + * one coming last in the source text) so that we reliably display the + * same error in all compilers. + */ + val validPositions = alts.collect { + case alt if alt.sym.owner == currentClass => alt.pos + } + val pos: SourcePosition = + if (validPositions.isEmpty) currentClass.sourcePos + else validPositions.maxBy(_.point) + + val kind = + if (currentClass.isJSType) "method" + else "exported method" + + val displayName = jsName.displayName + val altsTypesInfo = alts.map(_.typeInfo).mkString("\n ") + + report.error( + s"Cannot disambiguate overloads for $kind $displayName with types\n $altsTypesInfo", + pos) + } + + /** Generates a call to the method represented by the given `exported` while using the formalArguments + * and potentially the argument array. + * + * Also inserts default parameters if required. + */ + private def genApplyForSingleExported(formalArgsRegistry: FormalArgsRegistry, + exported: Exported, static: Boolean): js.Tree = { + if (currentClassSym.isJSType && exported.sym.owner != currentClassSym.get) { + assert(!static, s"nonsensical JS super call in static export of ${exported.sym}") + genApplyForSingleExportedJSSuperCall(formalArgsRegistry, exported) + } else { + genApplyForSingleExportedNonJSSuperCall(formalArgsRegistry, exported, static) + } + } + + private def genApplyForSingleExportedJSSuperCall( + formalArgsRegistry: FormalArgsRegistry, exported: Exported): js.Tree = { + implicit val pos = exported.pos + + val sym = exported.sym + assert(!sym.isClassConstructor, + s"Trying to genApplyForSingleExportedJSSuperCall for the constructor ${sym.fullName}") + + val allArgs = formalArgsRegistry.genAllArgsRefsForForwarder() + + val superClass = { + val superClassSym = currentClassSym.asClass.superClass + if (superClassSym.isNestedJSClass) + js.VarRef(js.LocalIdent(JSSuperClassParamName))(jstpe.AnyType) + else + js.LoadJSConstructor(encodeClassName(superClassSym)) + } + + val receiver = js.This()(jstpe.AnyType) + val nameTree = genExpr(sym.jsName) + + if (sym.isJSGetter) { + assert(allArgs.isEmpty, + s"getter symbol $sym does not have a getter signature") + js.JSSuperSelect(superClass, receiver, nameTree) + } else if (sym.isJSSetter) { + assert(allArgs.size == 1 && allArgs.head.isInstanceOf[js.Tree], + s"setter symbol $sym does not have a setter signature") + js.Assign(js.JSSuperSelect(superClass, receiver, nameTree), + allArgs.head.asInstanceOf[js.Tree]) + } else { + js.JSSuperMethodCall(superClass, receiver, nameTree, allArgs) + } + } + + private def genApplyForSingleExportedNonJSSuperCall( + formalArgsRegistry: FormalArgsRegistry, exported: Exported, static: Boolean): js.Tree = { + + implicit val pos = exported.pos + + // the (single) type of the repeated parameter if any + val repeatedTpe = exported.params.lastOption.withFilter(_.isRepeated).map(_.info) + + val normalArgc = exported.params.size - (if (repeatedTpe.isDefined) 1 else 0) + + // optional repeated parameter list + val jsVarArgPrep = repeatedTpe map { tpe => + val rhs = genJSArrayToVarArgs(formalArgsRegistry.genVarargRef(normalArgc)) + val ident = freshLocalIdent("prep" + normalArgc) + js.VarDef(ident, NoOriginalName, rhs.tpe, mutable = false, rhs) + } + + // normal arguments + val jsArgRefs = + (0 until normalArgc).toList.map(formalArgsRegistry.genArgRef(_)) + + // Generate JS code to prepare arguments (default getters and unboxes) + val jsArgPrep = genPrepareArgs(jsArgRefs, exported, static) ++ jsVarArgPrep + val jsArgPrepRefs = jsArgPrep.map(_.ref) + + // Combine prep'ed formal arguments with captures + val allJSArgs = { + exported.captureParamsFront.map(_.ref) ::: + jsArgPrepRefs ::: + exported.captureParamsBack.map(_.ref) + } + + val jsResult = genResult(exported, allJSArgs, static) + + js.Block(jsArgPrep :+ jsResult) + } + + /** Generate the necessary JavaScript code to prepare the arguments of an + * exported method (unboxing and default parameter handling) + */ + private def genPrepareArgs(jsArgs: List[js.Tree], exported: Exported, static: Boolean)( + implicit pos: SourcePosition): List[js.VarDef] = { + + val result = new mutable.ListBuffer[js.VarDef] + + for { + (jsArg, (param, i)) <- jsArgs.zip(exported.params.zipWithIndex) + } yield { + // Unboxed argument (if it is defined) + val unboxedArg = unbox(jsArg, param.info) + + // If argument is undefined and there is a default getter, call it + val verifiedOrDefault = if (param.hasDefault) { + js.If(js.BinaryOp(js.BinaryOp.===, jsArg, js.Undefined()), { + genCallDefaultGetter(exported.sym, i, param.sym.sourcePos, static) { + prevArgsCount => result.take(prevArgsCount).toList.map(_.ref) + } + }, { + // Otherwise, unbox the argument + unboxedArg + })(unboxedArg.tpe) + } else { + // Otherwise, it is always the unboxed argument + unboxedArg + } + + result += js.VarDef(freshLocalIdent("prep" + i), NoOriginalName, + verifiedOrDefault.tpe, mutable = false, verifiedOrDefault) + } + + result.toList + } + + private def genCallDefaultGetter(sym: Symbol, paramIndex: Int, paramPos: SourcePosition, static: Boolean)( + previousArgsValues: Int => List[js.Tree])( + implicit pos: SourcePosition): js.Tree = { + + val targetSym = targetSymForDefaultGetter(sym) + val defaultGetterDenot = this.defaultGetterDenot(targetSym, sym, paramIndex) + + assert(defaultGetterDenot.exists, s"need default getter for method ${sym.fullName}") + assert(!defaultGetterDenot.isOverloaded, i"found overloaded default getter $defaultGetterDenot") + val defaultGetter = defaultGetterDenot.symbol + + val targetTree = + if (sym.isClassConstructor || static) genLoadModule(targetSym) + else js.This()(encodeClassType(targetSym)) + + // Pass previous arguments to defaultGetter + val defaultGetterArgs = previousArgsValues(defaultGetter.info.paramInfoss.head.size) + + if (targetSym.isJSType) { + if (defaultGetter.owner.isNonNativeJSClass) { + genApplyJSClassMethod(targetTree, defaultGetter, defaultGetterArgs) + } else { + report.error( + "When overriding a native method with default arguments, " + + "the overriding method must explicitly repeat the default arguments.", + paramPos) + js.Undefined() + } + } else { + genApplyMethod(targetTree, defaultGetter, defaultGetterArgs) + } + } + + private def targetSymForDefaultGetter(sym: Symbol): Symbol = { + if (sym.isClassConstructor) { + /*/* Get the companion module class. + * For inner classes the sym.owner.companionModule can be broken, + * therefore companionModule is fetched at uncurryPhase. + */ + val companionModule = enteringPhase(currentRun.namerPhase) { + sym.owner.companionModule + } + companionModule.moduleClass*/ + sym.owner.companionModule.moduleClass + } else { + sym.owner + } + } + + private def defaultGetterDenot(targetSym: Symbol, sym: Symbol, paramIndex: Int): Denotation = + targetSym.info.member(DefaultGetterName(sym.name.asTermName, paramIndex)) + + private def defaultGetterDenot(sym: Symbol, paramIndex: Int): Denotation = + defaultGetterDenot(targetSymForDefaultGetter(sym), sym, paramIndex) + + /** Generate the final forwarding call to the exported method. */ + private def genResult(exported: Exported, args: List[js.Tree], static: Boolean)( + implicit pos: SourcePosition): js.Tree = { + + val sym = exported.sym + + def receiver = { + if (static) + genLoadModule(sym.owner) + else if (sym.owner == defn.ObjectClass) + js.This()(jstpe.ClassType(ir.Names.ObjectClass)) + else + js.This()(encodeClassType(sym.owner)) + } + + def boxIfNeeded(call: js.Tree): js.Tree = { + box(call, atPhase(elimErasedValueTypePhase)(sym.info.resultType)) + } + + if (currentClassSym.isNonNativeJSClass) { + assert(sym.owner == currentClassSym.get, sym.fullName) + boxIfNeeded(genApplyJSClassMethod(receiver, sym, args)) + } else { + if (sym.isClassConstructor) + js.New(encodeClassName(currentClassSym), encodeMethodSym(sym), args) + else if (sym.isPrivate) + boxIfNeeded(genApplyMethodStatically(receiver, sym, args)) + else + boxIfNeeded(genApplyMethod(receiver, sym, args)) + } + } + + private def genThrowTypeError(msg: String = "No matching overload")(implicit pos: Position): js.Tree = + js.Throw(js.JSNew(js.JSGlobalRef("TypeError"), js.StringLiteral(msg) :: Nil)) + + private final class ParamSpec(val sym: Symbol, val info: Type, + val isRepeated: Boolean, val hasDefault: Boolean) { + override def toString(): String = + s"ParamSpec(${sym.name}, ${info.show}, isRepeated = $isRepeated, hasDefault = $hasDefault)" + } + + private object ParamSpec { + def apply(methodSym: Symbol, sym: Symbol, infoAtElimRepeated: Type, infoAtElimEVT: Type, + methodHasDefaultParams: Boolean, paramIndex: Int): ParamSpec = { + val isRepeated = infoAtElimRepeated.isRepeatedParam + val info = if (isRepeated) infoAtElimRepeated.repeatedToSingle else infoAtElimEVT + val hasDefault = methodHasDefaultParams && defaultGetterDenot(methodSym, paramIndex).exists + new ParamSpec(sym, info, isRepeated, hasDefault) + } + } + + // This is a case class because we rely on its structural equality + private final case class Exported(sym: Symbol) { + private val isAnonJSClassConstructor = + //sym.isClassConstructor && sym.owner.isAnonymousClass && isJSType(sym.owner) + false + + val isLiftedJSConstructor = + sym.isClassConstructor && sym.owner.isNestedJSClass + + val (params, captureParamsFront, captureParamsBack) = { + val paramNamessNow = sym.info.paramNamess + val paramInfosNow = sym.info.paramInfoss.flatten + val paramSymsAtElimRepeated = atPhase(elimRepeatedPhase)(sym.paramSymss.flatten.filter(_.isTerm)) + val (paramNamessAtElimRepeated, paramInfosAtElimRepeated, methodHasDefaultParams) = + atPhase(elimRepeatedPhase)((sym.info.paramNamess, sym.info.paramInfoss.flatten, sym.hasDefaultParams)) + val (paramNamessAtElimEVT, paramInfosAtElimEVT) = + atPhase(elimErasedValueTypePhase)((sym.info.paramNamess, sym.info.paramInfoss.flatten)) + + def buildFormalParams(paramSyms: List[Symbol], paramInfosAtElimRepeated: List[Type], + paramInfosAtElimEVT: List[Type]): IndexedSeq[ParamSpec] = { + (for { + (paramSym, infoAtElimRepeated, infoAtElimEVT, paramIndex) <- + paramSyms.lazyZip(paramInfosAtElimRepeated).lazyZip(paramInfosAtElimEVT).lazyZip(0 until paramSyms.size) + } yield { + ParamSpec(sym, paramSym, infoAtElimRepeated, infoAtElimEVT, methodHasDefaultParams, paramIndex) + }).toIndexedSeq + } + + if (!isLiftedJSConstructor && !isAnonJSClassConstructor) { + // Easy case: all params are formal params. + assert(paramInfosAtElimRepeated.size == paramInfosAtElimEVT.size, + s"Found ${paramInfosAtElimRepeated.size} params entering elimRepeated but " + + s"${paramInfosAtElimEVT.size} params entering elimErasedValueType for " + + s"non-lifted symbol ${sym.fullName}") + val formalParams = buildFormalParams(paramSymsAtElimRepeated, paramInfosAtElimRepeated, paramInfosAtElimEVT) + (formalParams, Nil, Nil) + } else { + /* The `arg$outer` param is added by erasure, following "instructions" + * by explicitouter, while the other capture params are added by + * lambdalift (between elimErasedValueType and now). + * + * scalac: Note that lambdalift creates new symbols even for parameters that + * are not the result of lambda lifting, but it preserves their + * `name`s. + */ + + val hasOuterParam = { + paramInfosAtElimEVT.size == paramInfosAtElimRepeated.size + 1 && + paramNamessAtElimEVT.flatten.head == nme.OUTER + } + assert( + hasOuterParam || paramInfosAtElimEVT.size == paramInfosAtElimRepeated.size, + s"Found ${paramInfosAtElimRepeated.size} params entering elimRepeated but " + + s"${paramInfosAtElimEVT.size} params entering elimErasedValueType for " + + s"lifted constructor symbol ${sym.fullName}") + + val startOfFormalParams = paramNamessNow.flatten.indexOfSlice(paramNamessAtElimRepeated.flatten) + val formalParamCount = paramInfosAtElimRepeated.size + + val nonOuterParamInfosAtElimEVT = + if (hasOuterParam) paramInfosAtElimEVT.tail + else paramInfosAtElimEVT + val formalParams = buildFormalParams(paramSymsAtElimRepeated, paramInfosAtElimRepeated, nonOuterParamInfosAtElimEVT) + + val paramNamesAndInfosNow = paramNamessNow.flatten.zip(paramInfosNow) + val (captureParamsFrontNow, restOfParamsNow) = paramNamesAndInfosNow.splitAt(startOfFormalParams) + val captureParamsBackNow = restOfParamsNow.drop(formalParamCount) + + def makeCaptureParamDef(nameAndInfo: (TermName, Type)): js.ParamDef = { + implicit val pos: Position = sym.span + js.ParamDef(freshLocalIdent(nameAndInfo._1.mangledString), NoOriginalName, toIRType(nameAndInfo._2), + mutable = false, rest = false) + } + + val captureParamsFront = captureParamsFrontNow.map(makeCaptureParamDef(_)) + val captureParamsBack = captureParamsBackNow.map(makeCaptureParamDef(_)) + + /*if (isAnonJSClassConstructor) { + // For an anonymous JS class constructor, we put the capture parameters back as formal parameters. + val allFormalParams = captureParamsFront.toIndexedSeq ++ formalParams ++ captureParamsBack.toIndexedSeq + (allFormalParams, Nil, Nil) + } else {*/ (formalParams, captureParamsFront, captureParamsBack) + //} + } + } + + val hasRepeatedParam = params.nonEmpty && params.last.isRepeated + + def pos: SourcePosition = sym.sourcePos + + def exportArgTypeAt(paramIndex: Int): Type = { + if (paramIndex < params.length) { + params(paramIndex).info + } else { + assert(hasRepeatedParam, i"$sym does not have varargs nor enough params for $paramIndex") + params.last.info + } + } + + def typeInfo: String = sym.info.toString + } + + private sealed abstract class RTTypeTest + + private case class PrimitiveTypeTest(tpe: jstpe.Type, rank: Int) extends RTTypeTest + + private case class InstanceOfTypeTest(tpe: Type) extends RTTypeTest { + override def equals(that: Any): Boolean = { + that match { + case InstanceOfTypeTest(thatTpe) => tpe =:= thatTpe + case _ => false + } + } + } + + private case object NoTypeTest extends RTTypeTest + + private object RTTypeTest { + given PartialOrdering[RTTypeTest] { + override def tryCompare(lhs: RTTypeTest, rhs: RTTypeTest): Option[Int] = { + if (lteq(lhs, rhs)) if (lteq(rhs, lhs)) Some(0) else Some(-1) + else if (lteq(rhs, lhs)) Some(1) else None + } + + override def lteq(lhs: RTTypeTest, rhs: RTTypeTest): Boolean = { + (lhs, rhs) match { + // NoTypeTest is always last + case (_, NoTypeTest) => true + case (NoTypeTest, _) => false + + case (PrimitiveTypeTest(_, rank1), PrimitiveTypeTest(_, rank2)) => + rank1 <= rank2 + + case (InstanceOfTypeTest(t1), InstanceOfTypeTest(t2)) => + t1 <:< t2 + + case (_: PrimitiveTypeTest, _: InstanceOfTypeTest) => true + case (_: InstanceOfTypeTest, _: PrimitiveTypeTest) => false + } + } + + override def equiv(lhs: RTTypeTest, rhs: RTTypeTest): Boolean = { + lhs == rhs + } + } + } + + /** Very simple O(n²) topological sort for elements assumed to be distinct. */ + private def topoSortDistinctsBy[A <: AnyRef, B](coll: List[A])(f: A => B)( + using ord: PartialOrdering[B]): List[A] = { + + @tailrec + def loop(coll: List[A], acc: List[A]): List[A] = { + if (coll.isEmpty) acc + else if (coll.tail.isEmpty) coll.head :: acc + else { + val (lhs, rhs) = coll.span(x => !coll.forall(y => (x eq y) || !ord.lteq(f(x), f(y)))) + assert(!rhs.isEmpty, s"cycle while ordering $coll") + loop(lhs ::: rhs.tail, rhs.head :: acc) + } + } + + loop(coll, Nil) + } + + private def typeTestForTpe(tpe: Type): RTTypeTest = { + tpe match { + case tpe: ErasedValueType => + InstanceOfTypeTest(tpe.tycon.typeSymbol.typeRef) + + case _ => + import org.scalajs.ir.Names + + (toIRType(tpe): @unchecked) match { + case jstpe.AnyType => NoTypeTest + + case jstpe.NoType => PrimitiveTypeTest(jstpe.UndefType, 0) + case jstpe.BooleanType => PrimitiveTypeTest(jstpe.BooleanType, 1) + case jstpe.CharType => PrimitiveTypeTest(jstpe.CharType, 2) + case jstpe.ByteType => PrimitiveTypeTest(jstpe.ByteType, 3) + case jstpe.ShortType => PrimitiveTypeTest(jstpe.ShortType, 4) + case jstpe.IntType => PrimitiveTypeTest(jstpe.IntType, 5) + case jstpe.LongType => PrimitiveTypeTest(jstpe.LongType, 6) + case jstpe.FloatType => PrimitiveTypeTest(jstpe.FloatType, 7) + case jstpe.DoubleType => PrimitiveTypeTest(jstpe.DoubleType, 8) + + case jstpe.ClassType(Names.BoxedUnitClass) => PrimitiveTypeTest(jstpe.UndefType, 0) + case jstpe.ClassType(Names.BoxedStringClass) => PrimitiveTypeTest(jstpe.StringType, 9) + case jstpe.ClassType(_) => InstanceOfTypeTest(tpe) + + case jstpe.ArrayType(_) => InstanceOfTypeTest(tpe) + } + } + } + + // Group-by that does not rely on hashCode(), only equals() - O(n²) + private def groupByWithoutHashCode[A, B](coll: List[A])(f: A => B): List[(B, List[A])] = { + val m = new mutable.ArrayBuffer[(B, List[A])] + m.sizeHint(coll.length) + + for (elem <- coll) { + val key = f(elem) + val index = m.indexWhere(_._1 == key) + if (index < 0) + m += ((key, List(elem))) + else + m(index) = (key, elem :: m(index)._2) + } + + m.toList + } + + private class FormalArgsRegistry(minArgc: Int, needsRestParam: Boolean) { + private val fixedParamNames: scala.collection.immutable.IndexedSeq[jsNames.LocalName] = + (0 until minArgc).toIndexedSeq.map(_ => freshLocalIdent("arg")(NoPosition).name) + + private val restParamName: jsNames.LocalName = + if (needsRestParam) freshLocalIdent("rest")(NoPosition).name + else null + + def genFormalArgs()(implicit pos: Position): List[js.ParamDef] = { + val fixedParamDefs = fixedParamNames.toList.map { paramName => + js.ParamDef(js.LocalIdent(paramName), NoOriginalName, jstpe.AnyType, mutable = false, rest = false) + } + + if (needsRestParam) { + val restParamDef = + js.ParamDef(js.LocalIdent(restParamName), NoOriginalName, jstpe.AnyType, mutable = false, rest = true) + fixedParamDefs :+ restParamDef + } else { + fixedParamDefs + } + } + + def genArgRef(index: Int)(implicit pos: Position): js.Tree = { + if (index < minArgc) + js.VarRef(js.LocalIdent(fixedParamNames(index)))(jstpe.AnyType) + else + js.JSSelect(genRestArgRef(), js.IntLiteral(index - minArgc)) + } + + def genVarargRef(fixedParamCount: Int)(implicit pos: Position): js.Tree = { + assert(fixedParamCount >= minArgc, s"genVarargRef($fixedParamCount) with minArgc = $minArgc at $pos") + val restParam = genRestArgRef() + if (fixedParamCount == minArgc) + restParam + else + js.JSMethodApply(restParam, js.StringLiteral("slice"), List(js.IntLiteral(fixedParamCount - minArgc))) + } + + def genRestArgRef()(implicit pos: Position): js.Tree = { + assert(needsRestParam, s"trying to generate a reference to non-existent rest param at $pos") + js.VarRef(js.LocalIdent(restParamName))(jstpe.AnyType) + } + + def genAllArgsRefsForForwarder()(implicit pos: Position): List[js.Tree] = { + val fixedArgRefs = fixedParamNames.toList.map { paramName => + js.VarRef(js.LocalIdent(paramName))(jstpe.AnyType) + } + + if (needsRestParam) { + val restArgRef = js.VarRef(js.LocalIdent(restParamName))(jstpe.AnyType) + fixedArgRefs :+ restArgRef + } else { + fixedArgRefs + } + } + } +} diff --git a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala index 1beb9bdf30aa..d4a96f29ca5c 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala @@ -42,7 +42,10 @@ object JSPrimitives { final val THROW = DEBUGGER + 1 - final val REFLECT_SELECTABLE_SELECTDYN = THROW + 1 // scala.reflect.Selectable.selectDynamic + final val UNION_FROM = THROW + 1 // js.|.from + final val UNION_FROM_TYPE_CONSTRUCTOR = UNION_FROM + 1 // js.|.fromTypeConstructor + + final val REFLECT_SELECTABLE_SELECTDYN = UNION_FROM_TYPE_CONSTRUCTOR + 1 // scala.reflect.Selectable.selectDynamic final val REFLECT_SELECTABLE_APPLYDYN = REFLECT_SELECTABLE_SELECTDYN + 1 // scala.reflect.Selectable.applyDynamic final val LastJSPrimitiveCode = REFLECT_SELECTABLE_APPLYDYN @@ -104,9 +107,9 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) { addPrimitive(defn.BoxedUnit_UNIT, UNITVAL) addPrimitive(jsdefn.Runtime_constructorOf, CONSTRUCTOROF) - /*addPrimitive(jsdefn.Runtime_createInnerJSClass, CREATE_INNER_JS_CLASS) + addPrimitive(jsdefn.Runtime_createInnerJSClass, CREATE_INNER_JS_CLASS) addPrimitive(jsdefn.Runtime_createLocalJSClass, CREATE_LOCAL_JS_CLASS) - addPrimitive(jsdefn.Runtime_withContextualJSClassValue, WITH_CONTEXTUAL_JS_CLASS_VALUE)*/ + addPrimitive(jsdefn.Runtime_withContextualJSClassValue, WITH_CONTEXTUAL_JS_CLASS_VALUE) addPrimitive(jsdefn.Runtime_linkingInfo, LINKING_INFO) addPrimitive(jsdefn.Special_strictEquals, STRICT_EQ) @@ -118,6 +121,9 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) { addPrimitive(defn.throwMethod, THROW) + addPrimitive(jsdefn.PseudoUnion_from, UNION_FROM) + addPrimitive(jsdefn.PseudoUnion_fromTypeConstructor, UNION_FROM_TYPE_CONSTRUCTOR) + addPrimitive(jsdefn.ReflectSelectable_selectDynamic, REFLECT_SELECTABLE_SELECTDYN) addPrimitive(jsdefn.ReflectSelectable_applyDynamic, REFLECT_SELECTABLE_APPLYDYN) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index e8726f9eb73b..d363228f2d2f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -95,6 +95,8 @@ object TypeErasure { abstract case class ErasedValueType(tycon: TypeRef, erasedUnderlying: Type) extends CachedGroundType with ValueType { override def computeHash(bs: Hashable.Binders): Int = doHash(bs, tycon, erasedUnderlying) + + final def valueClassSymbol(using Context): ClassSymbol = tycon.typeSymbol.asClass } final class CachedErasedValueType(tycon: TypeRef, erasedUnderlying: Type) diff --git a/compiler/src/dotty/tools/dotc/transform/Constructors.scala b/compiler/src/dotty/tools/dotc/transform/Constructors.scala index ae7acc49eab1..a46469dddb44 100644 --- a/compiler/src/dotty/tools/dotc/transform/Constructors.scala +++ b/compiler/src/dotty/tools/dotc/transform/Constructors.scala @@ -303,7 +303,10 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase = val finalConstrStats = copyParams ::: mappedSuperCalls ::: lazyAssignments ::: stats val expandedConstr = if (cls.isAllOf(NoInitsTrait)) { - assert(finalConstrStats.isEmpty) + assert(finalConstrStats.isEmpty || { + import dotty.tools.dotc.transform.sjs.JSSymUtils._ + ctx.settings.scalajs.value && cls.isJSType + }) constr } else cpy.DefDef(constr)(rhs = Block(finalConstrStats, unitLiteral)) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index a77916bd8aa1..685689d144d5 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -3,7 +3,12 @@ package dotc package transform package sjs +import scala.collection.mutable + import MegaPhase._ +import core.Annotations._ +import core.Constants._ +import core.Denotations._ import core.DenotTransformers._ import core.Symbols._ import core.Contexts._ @@ -12,22 +17,219 @@ import core.Types._ import core.Flags._ import core.Decorators._ import core.StdNames.nme +import core.SymDenotations.SymDenotation import core.Names._ +import core.NameKinds._ import core.NameOps._ import ast.Trees._ import SymUtils._ import dotty.tools.dotc.ast.tpd +import util.Store + import dotty.tools.backend.sjs.JSDefinitions.jsdefn +import JSSymUtils._ + /** This phase makes all JS classes explicit (their definitions and references to them). * - * Ultimately, this will be the equivalent of the two phases `ExplicitInnerJS` - * and `ExplicitLocalJS` from Scala 2. Currently, this phase only performs the - * following transformations: + * This phase is the equivalent of the two phases `ExplicitInnerJS` and + * `ExplicitLocalJS` from Scala 2. It performs the following transformations: + * + * (A) For every inner JS class `Inner` in a class or trait `Outer`, create a + * field `Outer.Inner\$jsclass` to hold the JS class value of `Inner`. + * (B) For every exposed object `Inner` in a static owner `Outer`, create an + * explicit exposed getter `Outer.Inner\$jsobject`. + * (C) For every local JS class `Local`, create a local val `Local\$jsclass` + * to hold the JS class value of `Local`. + * (D) Desugar calls like `x.isInstanceOf[C]` into + * `js.special.instanceof(x, js.constructorOf[C])` when `C` is a nested + * JS class. + * (E) Wrap every `new C` call and `super[C]` reference of a nested JS class + * `C` with `withContextualJSClassValue(js.constructorOf[C], ...)`. + * (F) Desugar calls to `js.constructorOf[C]` (including those generated by + * the previous transformations) into either `runtime.constructorOf` or + * access to the `\$jsclass` fields/vals. + * (G) Adjust the `NoInits` flag of traits: + * - for JS traits, always add the flag + * - for Scala trait that contain a JS class, remove the flag + * + * Note that in this comment, and more largely in this phase, by "class" we + * mean *only* `class`es. `trait`s and `object`s are not implied. + * + * -------------------------------------- + * + * (A) `Inner\$jsclass` fields + * + * Roughly, for every inner JS class of the form: + * {{{ + * class Outer { + * class Inner extends ParentJSClass + * } + * }}} + * this phase creates a field `Inner\$jsclass` in `Outer` to hold the JS class + * value for `Inner`. The rhs of that field is a call to a magic method, used + * to retain information that the back-end will need. + * {{{ + * class Outer { + * val Inner\$jsclass: AnyRef = + * createJSClass(classOf[Inner], js.constructorOf[ParentJSClass]) + * + * class Inner extends ParentJSClass + * } + * }}} + * + * These fields will be read by code generated in step (F). + * + * A `\$jsclass` field is also generated for classes declared inside *static + * JS objects*. Indeed, even though those classes have a unique, globally + * accessible class value, that class value needs to be *exposed* as a field + * of the enclosing object. In those cases, the rhs of the field is a direct + * call to `js.constructorOf[Inner]`, which becomes + * `runtime.constructorOf(classOf[Inner])`. + * + * For the following input: + * {{{ + * object Outer extends js.Object { + * class InnerClass extends ParentJSClass + * } + * }}} + * this phase will generate + * {{{ + * object Outer extends js.Object { + * @ExposedJSMember @JSName("InnerClass") + * val InnerClass\$jsclass: AnyRef = runtime.constructorOf(classOf[InnerClass]) + * } + * }}} + * + * The `\$jsclass` fields must also be added to outer classes and traits + * coming from separate compilation, therefore this phase is an + * `InfoTransform`. + * + * -------------------------------------- + * + * (B) `Inner\$jsobject` exposed getters + * + * For *modules* declared inside static JS objects, we generate an explicit + * exposed getter as well. For non-static objects, dotc already generates a + * getter with the `@ExposedJSMember` annotation, so we do not need to do + * anything. But for static objects, it doesn't, so we have to do it ourselves + * here. + * + * For the following input: + * {{{ + * object Outer extends js.Object { + * object InnerObject extends ParentJSClass + * } + * }}} + * this phase will generate + * {{{ + * object Outer extends js.Object { + * @ExposedJSMember @JSName("InnerObject") + * def InnerObject\$jsobject: AnyRef = InnerObject + * } + * }}} + * + * -------------------------------------- + * + * (C) `Local\$jsclass` vals and vars + * + * Similarly to how step (A) creates explicit fields in the enclosing + * templates of inner JS classes and traits to hold the JS class values, this + * phase creates local vals for local JS classes in the enclosing statement + * list. + * + * For every local JS class of the form: + * {{{ + * def outer() = { + * class Local extends ParentJSClass + * } + * }}} + * this phase creates a local `val Local\$jslass` in the body of `outer()` to + * hold the JS class value for `Local`. The rhs of that val is a call to a + * magic method, used to retain information that the back-end will need: + * + * - A reified reference to `class Local`, in the form of a `classOf` + * - An explicit reference to the super JS class value, i.e., the desugaring + * of `js.constructorOf[ParentJSClass]` + * - An array of fake `new` expressions for all overloaded constructors. + * + * The latter will be augmented by `LambdaLift` with the appropriate actual + * parameters for the captures of `Local`, which will be needed by the + * back-end. In code, this looks like: + * {{{ + * def outer() = { + * class Local extends ParentJSClass + * val Local\$jsclass: AnyRef = createLocalJSClass( + * classOf[Local], + * js.constructorOf[ParentJSClass], + * Array[AnyRef](new Local(), ...)) + * } + * }}} + * + * Since we need to insert fake `new Inner()`s, this scheme does not work for + * abstract local classes. We therefore reject them as implementation + * restriction in `PrepJSInterop`. + * + * If the body of `Local` references itself, then the `val Local\$jsclass` is + * instead declared as a `var` to work around the cyclic dependency: + * {{{ + * def outer() = { + * var Local\$jsclass: AnyRef = null + * class Local extends ParentJSClass { + * def newLocal = new Local // self-reference + * } + * Local\$jsclass = createLocalJSClass(...) + * } + * }}} + * + * -------------------------------------- + * + * (D) Insertion of `withContextualJSClassValue` calls + * + * For any nested JS class `C`, this phase performs the following + * transformations: + * + * - `new C[...Ts](...args)` desugars into + * `withContextualJSClassValue(js.constructorOf[C], new C[...Ts](...args))`, + * so that the back-end receives a reified reference to the JS class value. + * - In the same spirit, for `D extends C`, `D.super[C].m[...Ts](...args)` + * desugars into + * `withContextualJSClassValue(js.constructorOf[C], D.super[C].m[...Ts](...args))`. + * + * For any nested JS *object*, their (only) instantiation point of the form + * `new O$()` is rewritten as + * `withContextualJSClassValue(js.constructorOf[ParentClassOfO], new O$())`, + * so that the back-end receives a reified reference to the parent class of + * `O`. + * + * TODO A similar treatment is applied on anonymous JS classes, which + * basically define something very similar to an `object`, although without + * its own JS class. + * + * -------------------------------------- + * + * (E) Desugar `x.isInstanceOf[C]` for nested JS classes * - * - Rewrite `js.constructorOf[T]` into `scala.scalajs.runtime.constructorOf(classOf[T])`, - * where the `classOf[T]` is represented as a `Literal`. + * They are desugared into `js.special.instanceof(x, js.constructorOf[C])`. + * + * -------------------------------------- + * + * (F) Desugar `js.constructorOf[C]` + * + * Finally, this phase rewrites all calls to `js.constructorOf[C]`, including + * the ones generated by the previous steps. The transformation depends on the + * nature of `C`: + * + * - If `C` is a statically accessible class, desugar to + * `runtime.constructorOf(classOf[C])` so that the reified symbol survives + * erasure and reaches the back-end. + * - If `C` is an inner JS class, it must be of the form `path.D` for some + * pair (`path`, `D`), and we desugar it to `path.D\$jsclass`, using the + * field created by step (A) (it is an error if `C` is of the form + * `Enclosing#D`). + * - If `C` is a local JS class, desugar to `C\$jsclass`, using the local val + * created by step (C). */ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => import ExplicitJSClasses._ @@ -35,6 +237,12 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => override def phaseName: String = ExplicitJSClasses.name + private var MyState: Store.Location[MyState] = _ + private def myState(using Context) = ctx.store(MyState) + + override def initContext(ctx: FreshContext): Unit = + MyState = ctx.addLocation[MyState]() + override def isEnabled(using Context): Boolean = ctx.settings.scalajs.value @@ -42,24 +250,495 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => override def changesMembers: Boolean = true // the phase adds fields for inner JS classes - override def transformInfo(tp: Type, sym: Symbol)(using Context): Type = { - // Currently we don't do anything here. Eventually we'll add fields for inner JS classes. - tp + /** Is the given symbol an owner for which this transformation applies? + * + * This applies if either or both of the following are true: + * - It is not a static owner, or + * - It is a non-native JS object. + * + * The latter is necessary for scala-js/scala-js#4086. + */ + private def isApplicableOwner(sym: Symbol)(using Context): Boolean = { + !sym.isStaticOwner || ( + sym.is(ModuleClass) && + sym.hasAnnotation(jsdefn.JSTypeAnnot) && + !sym.hasAnnotation(jsdefn.JSNativeAnnot) + ) + } + + /** Is the given symbol a JS class (that is not a trait nor an object)? */ + private def isJSClass(sym: Symbol)(using Context): Boolean = { + sym.isClass && + !sym.isOneOf(Trait | Module) && + sym.hasAnnotation(jsdefn.JSTypeAnnot) + } + + /** Is the given symbol a Module that should be exposed? */ + private def isExposedModule(sym: Symbol)(using Context): Boolean = + sym.is(Module) && sym.hasAnnotation(jsdefn.ExposedJSMemberAnnot) + + /** Is the gen clazz an inner or local JS class? */ + private def isInnerOrLocalJSClass(sym: Symbol)(using Context): Boolean = + isInnerJSClass(sym) || isLocalJSClass(sym) + + /** Is the given clazz an inner JS class? */ + private def isInnerJSClass(clazz: Symbol)(using Context): Boolean = + isInnerJSClassOrObject(clazz) && !clazz.is(ModuleClass) + + /** Is the given clazz a local JS class? */ + private def isLocalJSClass(clazz: Symbol)(using Context): Boolean = + isLocalJSClassOrObject(clazz) && !clazz.is(ModuleClass) //&& !clazz.isAnonymousClass + + /** Is the gen clazz an inner or local JS class or object? */ + private def isInnerOrLocalJSClassOrObject(sym: Symbol)(using Context): Boolean = + isInnerJSClassOrObject(sym) || isLocalJSClassOrObject(sym) + + /** Is the given clazz an inner JS class or object? */ + private def isInnerJSClassOrObject(clazz: Symbol)(using Context): Boolean = { + clazz.hasAnnotation(jsdefn.JSTypeAnnot) + && !clazz.isOneOf(PackageClass | Trait) + && !clazz.isStatic + && !clazz.isLocalToBlock + } + + /** Is the given clazz a local JS class or object? */ + private def isLocalJSClassOrObject(clazz: Symbol)(using Context): Boolean = + clazz.isLocalToBlock && !clazz.is(Trait) && clazz.hasAnnotation(jsdefn.JSTypeAnnot) + + private def jsclassFieldName(clazzName: TypeName): TermName = + clazzName.toTermName ++ "$jsname" + + private def jsclassAccessorFor(clazz: Symbol)(using Context): TermSymbol = + clazz.owner.info.decls.lookup(jsclassFieldName(clazz.name.asTypeName)).asTerm + + private def jsobjectGetterName(moduleName: TermName): TermName = + moduleName ++ "$jsobject" + + private def jsobjectGetterNameFor(moduleSym: Symbol)(using Context): TermName = + jsobjectGetterName(moduleSym.name.asTermName) + + private def makeJSNameAnnotation(argument: String)(using Context): Annotation = { + val annotClass = jsdefn.JSNameAnnot + val stringCtor = annotClass.info.decl(nme.CONSTRUCTOR).suchThat { ctor => + ctor.info match { + case mt: MethodType => mt.paramInfos.nonEmpty && mt.paramInfos.head.derivesFrom(defn.StringClass) + case _ => false + } + }.symbol.asTerm + Annotation(New(annotClass.typeRef, stringCtor, Literal(Constant(argument)) :: Nil)) + } + + override def transformInfo(tp: Type, sym: Symbol)(using Context): Type = tp match { + case tp @ ClassInfo(_, cls, _, decls, _) if !cls.is(JavaDefined) && isApplicableOwner(cls) => + val innerJSClasses = decls.filter(isJSClass) + + val innerObjectsForAdHocExposed = + if (!cls.isStaticOwner) Nil // those already have a module accessor + else decls.filter(isExposedModule) + + if (innerJSClasses.isEmpty && innerObjectsForAdHocExposed.isEmpty) { + tp + } else { + def addAnnots(sym: Symbol, symForName: Symbol): Unit = { + val jsNameAnnot = symForName.getAnnotation(jsdefn.JSNameAnnot).getOrElse { + makeJSNameAnnotation(symForName.defaultJSName) + } + sym.addAnnotation(jsNameAnnot) + sym.addAnnotation(jsdefn.ExposedJSMemberAnnot) + } + + val clsIsJSClass = cls.hasAnnotation(jsdefn.JSTypeAnnot) + + val decls1 = decls.cloneScope + + for (innerJSClass <- innerJSClasses) { + def addAnnotsIfInJSClass(sym: Symbol): Unit = { + if (clsIsJSClass) + addAnnots(sym, innerJSClass) + } + + val fieldName = jsclassFieldName(innerJSClass.name.asTypeName) + val fieldFlags = Synthetic | Artifact + val field = newSymbol(cls, fieldName, fieldFlags, defn.AnyRefType, coord = innerJSClass.coord) + addAnnotsIfInJSClass(field) + decls1.enter(field) + } + + // scala-js/scala-js#4086 Create exposed getters for exposed objects in static JS objects + for (innerObject <- innerObjectsForAdHocExposed) { + assert(clsIsJSClass && cls.is(ModuleClass) && cls.isStatic, + i"trying to ad-hoc expose objects in non-JS static object ${cls.fullName}") + + val getterName = jsobjectGetterNameFor(innerObject) + val getterFlags = Method | Synthetic | Artifact + val getter = newSymbol(cls, getterName, getterFlags, ExprType(defn.AnyRefType), coord = innerObject.coord) + addAnnots(getter, innerObject) + decls1.enter(getter) + } + + tp.derivedClassInfo(decls = decls1) + } + + case _ => + tp + } + + /** Adjust the `NoInits` flag of Scala traits containing a JS class and of JS traits. */ + override def transform(ref: SingleDenotation)(using Context): SingleDenotation = { + super.transform(ref) match { + case ref1: SymDenotation if ref1.is(Trait, butNot = JavaDefined) => + val isJSType = ref1.hasAnnotation(jsdefn.JSTypeAnnot) + if (ref1.is(NoInits)) { + // If one of the decls is a JS class, there is now some initialization code to create the JS class + if (!isJSType && ref1.info.decls.exists(isJSClass)) + ref1.copySymDenotation(initFlags = ref1.flags &~ NoInits) + else + ref1 + } else { + // JS traits never have an initializer, no matter what dotc thinks + if (isJSType) + ref1.copySymDenotation(initFlags = ref1.flags | NoInits) + else + ref1 + } + case ref1 => + ref1 + } } override def infoMayChange(sym: Symbol)(using Context): Boolean = sym.isClass && !sym.is(JavaDefined) - override def transformTypeApply(tree: TypeApply)(using Context): tpd.Tree = { - tree match { - case TypeApply(fun, tpt :: Nil) if fun.symbol == jsdefn.JSPackage_constructorOf => - ref(jsdefn.Runtime_constructorOf).appliedTo(clsOf(tpt.tpe)) + override def prepareForUnit(tree: Tree)(using Context): Context = + ctx.fresh.updateStore(MyState, new MyState()) + + /** Populate `nestedObject2superClassTpe` for inner objects at the start of + * a `Block` or `Template`, so that they are visible even before their + * definition (in their enclosing scope). + */ + private def populateNestedObject2superClassTpe(stats: List[Tree])(using Context): Unit = { + for (stat <- stats) { + stat match { + case cd @ TypeDef(_, rhs) if cd.isClassDef && cd.symbol.is(ModuleClass) && isInnerOrLocalJSClassOrObject(cd.symbol) => + myState.nestedObject2superClassTpe(cd.symbol) = extractSuperTpeFromImpl(rhs.asInstanceOf[Template]) + case _ => + } + } + } + + override def prepareForBlock(tree: Block)(using Context): Context = { + populateNestedObject2superClassTpe(tree.stats) + ctx + } + + override def prepareForTemplate(tree: Template)(using Context): Context = { + populateNestedObject2superClassTpe(tree.body) + ctx + } + + // This method implements steps (A) and (B) + override def transformTemplate(tree: Template)(using Context): Tree = { + val cls = ctx.owner.asClass + + /* The `parents` of a Template have the same trees as `new` invocations + * of the parent classes and traits. That means that `transformApply` may + * have wrapped them in a `withContextualJSClassValue`, not knowing where + * they belong in the larger tree. + * We now unwrap those, canceling out that effect. + * TODO Is there a better way to do this? + */ + val fixedParents = + if (!cls.isJSType) tree.parents // fast path + else tree.parents.mapConserve(unwrapWithContextualJSClassValue(_)) + + if (!isApplicableOwner(cls)) { + if (fixedParents eq tree.parents) tree + else cpy.Template(tree)(parents = fixedParents) + } else { + val newDecls = List.newBuilder[Tree] + for (decl <- tree.body) { + val declSym = decl.symbol + if (declSym eq null) { + // not a member def, do nothing + } else if (isJSClass(declSym)) { + val jsclassAccessor = jsclassAccessorFor(declSym) + + val rhs = if (cls.hasAnnotation(jsdefn.JSNativeAnnot)) { + ref(jsdefn.JSPackage_native) + } else { + val clazzValue = clsOf(declSym.typeRef) + if (cls.isStaticOwner) { + // #4086 + ref(jsdefn.Runtime_constructorOf).appliedTo(clazzValue) + } else { + val parentTpe = extractSuperTpeFromImpl(decl.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]) + val superClassCtor = genJSConstructorOf(tree, parentTpe) + ref(jsdefn.Runtime_createInnerJSClass).appliedTo(clazzValue, superClassCtor) + } + } + + newDecls += ValDef(jsclassAccessor, rhs) + } else if (cls.isStaticOwner) { + // #4086 + if (isExposedModule(declSym)) { + val getter = cls.info.decls.lookup(jsobjectGetterNameFor(declSym)).asTerm + newDecls += DefDef(getter, ref(declSym)) + } + } + + newDecls += decl + } + + cpy.Template(tree)(tree.constr, fixedParents, Nil, tree.self, newDecls.result()) + } + } + + // This method, together with transformTypeDef, implements step (C) + override def prepareForTypeDef(tree: TypeDef)(using Context): Context = { + val sym = tree.symbol + if (sym.isClass && isLocalJSClass(sym)) { + val jsclassValName = LocalJSClassValueName.fresh(sym.name.toTermName) + val jsclassVal = newSymbol(ctx.owner, jsclassValName, EmptyFlags, defn.AnyRefType, coord = tree.span) + val state = myState + state.localClass2jsclassVal(sym) = jsclassVal + state.notYetSelfReferencingLocalClasses += sym + } + ctx + } + + // This method, together with prepareForTypeDef, implements step (C) + override def transformTypeDef(tree: TypeDef)(using Context): Tree = { + val sym = tree.symbol + if (sym.isClass && isLocalJSClass(sym)) { + val state = myState + val cls = sym.asClass + + val rhs = { + val typeRef = tree.tpe + val clazzValue = clsOf(typeRef) + val superClassCtor = genJSConstructorOf(tree, extractSuperTpeFromImpl(tree.rhs.asInstanceOf[Template])) + val fakeNewInstances = { + /* We need to use `reverse` because the Scope returns elements in reverse order compared to tree definitions. + * The back-end needs the fake News to be in the same order as the corresponding tree definitions. + */ + val ctors = cls.info.decls.lookupAll(nme.CONSTRUCTOR).toList.reverse + val elems = ctors.map(ctor => fakeNew(cls, ctor.asTerm)) + JavaSeqLiteral(elems, TypeTree(defn.AnyRefType)) + } + ref(jsdefn.Runtime_createLocalJSClass).appliedTo(clazzValue, superClassCtor, fakeNewInstances) + } + + val jsclassVal = state.localClass2jsclassVal(sym) + if (state.notYetSelfReferencingLocalClasses.remove(cls)) { + Thicket(List(tree, ValDef(jsclassVal, rhs))) + } else { + /* We are using `jsclassVal` inside the definition of the class. + * We need to declare it as var before and initialize it after the class definition. + */ + jsclassVal.setFlag(Mutable) + Thicket(List( + ValDef(jsclassVal, Literal(Constant(null))), + tree, + Assign(ref(jsclassVal), rhs) + )) + } + } else { + tree + } + } + + /** Creates a fake invocation of the the given class with the given constructor. */ + def fakeNew(cls: ClassSymbol, ctor: TermSymbol)(using Context): Tree = { + /* TODO This is not entirely good enough, as it break -Ycheck for generic + * classes. Erasure restores the consistency of the fake invocations. + * Improving this is left for later. + */ + + val tycon = cls.typeRef + val targs = cls.typeParams.map(_ => TypeBounds.emptyPolyKind) + val argss = ctor.info.paramInfoss.map(_.map(_ => ref(defn.Predef_undefined))) + + New(tycon) + .select(TermRef(tycon, ctor)) + .appliedToTypes(targs) + .appliedToArgss(argss) + } + + // This method, together with transformTypeApply and transformSelect, implements step (E) + override def transformApply(tree: Apply)(using Context): Tree = { + if (!isFullyApplied(tree)) { + tree + } else { + val sym = tree.symbol + + if (sym.isConstructor) { + /* Wrap `new`s to inner and local JS classes and objects with + * `withContextualJSClassValue`, to preserve a reified reference to + * the necessary JS class value (the class itself for classes, or the + * super class for objects). + * Anonymous classes are considered as "objects" for this purpose. + */ + val cls = sym.owner + if (isInnerOrLocalJSClassOrObject(cls)) { + if (!cls.is(ModuleClass) /*&& !cls.isAnonymousClass*/) { + methPart(tree) match { + case Select(n @ New(tpt), _) => + val jsclassValue = genJSConstructorOf(tpt, n.tpe) + wrapWithContextualJSClassValue(jsclassValue)(tree) + case _ => + // Super constructor call or this()-constructor call + tree + } + } else { + wrapWithContextualJSClassValue(myState.nestedObject2superClassTpe(cls))(tree) + } + } else { + tree + } + } else { + maybeWrapSuperCallWithContextualJSClassValue(tree) + } + } + } + + // This method, together with transformApply and transformSelect, implements step (E) + // It also implements step (D) and (F) + override def transformTypeApply(tree: TypeApply)(using Context): Tree = { + if (!isFullyApplied(tree)) { + tree + } else { + val sym = tree.symbol + + def isTypeTreeForInnerOrLocalJSClass(tpeArg: Tree): Boolean = { + val tpeSym = tpeArg.tpe.typeSymbol + tpeSym.exists && isInnerOrLocalJSClass(tpeSym) + } + + tree match { + // Desugar js.constructorOf[T] + case TypeApply(fun, tpt :: Nil) if sym == jsdefn.JSPackage_constructorOf => + genJSConstructorOf(tree, tpt.tpe).cast(jsdefn.JSDynamicType) + + // Translate x.isInstanceOf[T] for inner and local JS classes + case TypeApply(fun @ Select(obj, _), tpeArg :: Nil) + if sym == defn.Any_isInstanceOf && isTypeTreeForInnerOrLocalJSClass(tpeArg) => + val jsCtorOf = genJSConstructorOf(tree, tpeArg.tpe) + ref(jsdefn.Special_instanceof).appliedTo(obj, jsCtorOf) + + case _ => + maybeWrapSuperCallWithContextualJSClassValue(tree) + } + } + } + + // This method, together with transformApply and transformTypeApply, implements step (E) + override def transformSelect(tree: Select)(using Context): Tree = { + if (!isFullyApplied(tree)) { + tree + } else { + maybeWrapSuperCallWithContextualJSClassValue(tree) + } + } + + /** Tests whether this tree is fully applied, i.e., it does not need any + * additional `TypeApply` or `Apply` to lead to a value. + * + * In this phase, `transformApply`, `transformTypeApply` and `transformSelect` + * must only operate on fully applied selections and applications. + */ + private def isFullyApplied(tree: Tree)(using Context): Boolean = { + tree.tpe.widenTermRefExpr match { + case _:PolyType | _:MethodType => false + case _ => true + } + } + + /** Wraps `super` calls to inner and local JS classes with + * `withContextualJSClassValue`, to preserve a reified reference to the + * necessary JS class value (that of the super class). + */ + private def maybeWrapSuperCallWithContextualJSClassValue(tree: Tree)(using Context): Tree = { + methPart(tree) match { + case Select(sup: Super, _) if isInnerOrLocalJSClass(sup.symbol.asClass.superClass) => + wrapWithContextualJSClassValue(sup.symbol.asClass.superClass.typeRef)(tree) case _ => tree } } + + /** Generates the desugared version of `js.constructorOf[tpe]`. + * + * This is the meat of step (F). + */ + private def genJSConstructorOf(tree: Tree, tpe0: Type)(using Context): Tree = { + val tpe = tpe0.underlyingClassRef(refinementOK = false) match { + case typeRef: TypeRef => typeRef + case _ => + // This should not have passed the checks in PrepJSInterop + report.error(i"class type required but found $tpe0", tree) + jsdefn.JSObjectType + } + val cls = tpe.typeSymbol + + // This should not have passed the checks in PrepJSInterop + assert(!cls.isOneOf(Trait | ModuleClass), + i"non-trait class type required but $tpe found for genJSConstructorOf at ${tree.sourcePos}") + + if (isInnerJSClass(cls)) { + // Use the $jsclass field in the outer instance + val prefix: Type = tpe.prefix + if (prefix.isStable) { + val jsclassAccessor = jsclassAccessorFor(cls) + ref(NamedType(prefix, jsclassAccessor.name, jsclassAccessor.denot)) + } else { + report.error(i"stable reference to a JS class required but $tpe found", tree) + ref(defn.Predef_undefined) + } + } else if (isLocalJSClass(cls)) { + // Use the local `val` that stores the JS class value + val state = myState + val jsclassVal = state.localClass2jsclassVal(cls) + state.notYetSelfReferencingLocalClasses -= cls + ref(jsclassVal) + } else { + // Defer translation to `LoadJSConstructor` to the back-end + ref(jsdefn.Runtime_constructorOf).appliedTo(clsOf(tpe)) + } + } + + private def wrapWithContextualJSClassValue(jsClassType: Type)(tree: Tree)(using Context): Tree = + wrapWithContextualJSClassValue(genJSConstructorOf(tree, jsClassType))(tree) + + private def wrapWithContextualJSClassValue(jsClassValue: Tree)(tree: Tree)(using Context): Tree = + ref(jsdefn.Runtime_withContextualJSClassValue).appliedToType(tree.tpe).appliedTo(jsClassValue, tree) + + private def unwrapWithContextualJSClassValue(tree: Tree)(using Context): Tree = tree match { + case Apply(fun, jsClassValue :: actualTree :: Nil) + if fun.symbol == jsdefn.Runtime_withContextualJSClassValue => + actualTree + case _ => + tree + } + + /** Extracts the super type constructor of a `Template`, without type + * parameters, so that the type is well-formed outside of the `Template`, + * i.e., at the same level where the corresponding `TypeDef` is defined. + * It is not necessarily *-kinded, though, which limits its applicability. + */ + private def extractSuperTpeFromImpl(impl: Template)(using Context): Type = { + // TODO Check whether stripPoly is the right thing. Do we need a sort of rawTypeRef? + impl.parents.head.tpe.stripPoly + } } object ExplicitJSClasses { val name: String = "explicitJSClasses" + + val LocalJSClassValueName: UniqueNameKind = new UniqueNameKind("$jsclass") + + private final class MyState { + val nestedObject2superClassTpe = new MutableSymbolMap[Type] + val localClass2jsclassVal = new MutableSymbolMap[TermSymbol] + val notYetSelfReferencingLocalClasses = new util.HashSet[Symbol] + } } diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala index 3d5cead775d0..b769b77b6d5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala @@ -53,15 +53,16 @@ object JSSymUtils { def isNonNativeJSClass(using Context): Boolean = sym.isJSType && !sym.hasAnnotation(jsdefn.JSNativeAnnot) + def isNestedJSClass(using Context): Boolean = + !sym.isStatic /*&& !isStaticModule(sym.originalOwner)*/ && sym.isJSType + /** Tests whether the given member is exposed, i.e., whether it was * originally a public or protected member of a non-native JS class. */ def isJSExposed(using Context): Boolean = { !sym.is(Bridge) && { - if (sym.is(Lazy)) - sym.is(Accessor) && sym.field.hasAnnotation(jsdefn.ExposedJSMemberAnnot) - else - sym.hasAnnotation(jsdefn.ExposedJSMemberAnnot) + sym.hasAnnotation(jsdefn.ExposedJSMemberAnnot) + || (sym.is(Accessor) && sym.field.hasAnnotation(jsdefn.ExposedJSMemberAnnot)) } } @@ -77,6 +78,10 @@ object JSSymUtils { def isJSSetter(using Context): Boolean = sym.originalName.isSetterName && sym.is(Method) + /** Is this symbol a JS getter or setter? */ + def isJSProperty(using Context): Boolean = + sym.isJSGetter || sym.isJSSetter + /** Should this symbol be translated into a JS bracket access? */ def isJSBracketAccess(using Context): Boolean = sym.hasAnnotation(jsdefn.JSBracketAccessAnnot) @@ -94,17 +99,13 @@ object JSSymUtils { def isJSDefaultParam(using Context): Boolean = { sym.name.is(DefaultGetterName) && { val owner = sym.owner - if (owner.is(ModuleClass)) { - val isConstructor = sym.name match { - case DefaultGetterName(methName, _) => methName == nme.CONSTRUCTOR - case _ => false - } - if (isConstructor) - owner.linkedClass.isJSType - else - owner.isJSType + val methName = sym.name.exclude(DefaultGetterName) + if (methName == nme.CONSTRUCTOR) { + owner.linkedClass.isJSType } else { - owner.isJSType + def isAttachedMethodExposed: Boolean = + owner.info.decl(methName).hasAltWith(_.symbol.isJSExposed) + owner.isJSType && (!owner.isNonNativeJSClass || isAttachedMethodExposed) } } } diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index 02b531b93113..55e6d145dfa2 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -106,7 +106,10 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP case tree: ValDef if tree.symbol.is(Module) => /* Never apply this transformation on the term definition of modules. * Instead, all relevant checks are performed on the module class definition. + * We still need to mark exposed if required, since that needs to be done + * on the module symbol, not its module class. */ + markExposedIfRequired(tree.symbol) super.transform(tree) case tree: MemberDef => transformMemberDef(tree) @@ -842,14 +845,18 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP */ private def markExposedIfRequired(sym: Symbol)(using Context): Unit = { val shouldBeExposed: Boolean = { + // it is a term member + sym.isTerm && // it is a member of a non-native JS class (enclosingOwner is OwnerKind.JSNonNative) && !sym.isLocalToBlock && - // it is a term member, and it is not synthetic - sym.isOneOf(Module | Method, butNot = Synthetic) && + // it is not synthetic + !sym.isOneOf(Synthetic) && // it is not private !isPrivateMaybeWithin(sym) && // it is not a constructor - !sym.isConstructor + !sym.isConstructor && + // it is not a default getter + !sym.name.is(DefaultGetterName) } if (shouldBeExposed) diff --git a/compiler/src/dotty/tools/dotc/util/HashSet.scala b/compiler/src/dotty/tools/dotc/util/HashSet.scala index 73e4c91c2be0..c856b8b01515 100644 --- a/compiler/src/dotty/tools/dotc/util/HashSet.scala +++ b/compiler/src/dotty/tools/dotc/util/HashSet.scala @@ -128,6 +128,13 @@ class HashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends Mu idx = nextIndex(idx) e = entryAt(idx) + def remove(x: T): Boolean = + if contains(x) then + this -= x + true + else + false + private def addOld(x: T) = Stats.record(statsItem("re-enter")) var idx = firstIndex(x) diff --git a/project/Build.scala b/project/Build.scala index aec06d8b81d0..93a8bbb4099d 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1019,8 +1019,7 @@ object Build { val dir = fetchScalaJSSource.value / "test-suite/js/src/main/scala" val filter = ( ("*.scala": FileFilter) - -- "Typechecking*.scala" - -- "NonNativeTypeTestSeparateRun.scala" + -- "Typechecking*.scala" // defines a Scala 2 macro ) (dir ** filter).get }, @@ -1038,29 +1037,18 @@ object Build { ++ (dir / "shared/src/test/require-jdk7" ** "*.scala").get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/compiler" ** (("*.scala": FileFilter) - -- "InteroperabilityTest.scala" // nested native JS classes + JS exports - -- "OptimizerTest.scala" // non-native JS classes - -- "RegressionJSTest.scala" // non-native JS classes + -- "InteroperabilityTest.scala" // 3 tests require JS exports, all other tests pass -- "RuntimeTypesTest.scala" // compile errors: no ClassTag for Null and Nothing )).get - ++ (dir / "js/src/test/scala/org/scalajs/testsuite/javalib" ** (("*.scala": FileFilter) - -- "ObjectJSTest.scala" // non-native JS classes - )).get + ++ (dir / "js/src/test/scala/org/scalajs/testsuite/javalib" ** "*.scala").get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/jsinterop" ** (("*.scala": FileFilter) - -- "AsyncTest.scala" // needs PromiseMock.scala + -- "AsyncTest.scala" // needs JS exports in PromiseMock.scala -- "DynamicTest.scala" // one test requires JS exports, all other tests pass -- "ExportsTest.scala" // JS exports - -- "IterableTest.scala" // non-native JS classes -- "JSExportStaticTest.scala" // JS exports - -- "JSOptionalTest.scala" // non-native JS classes - -- "JSSymbolTest.scala" // non-native JS classes - -- "MiscInteropTest.scala" // non-native JS classes - -- "ModulesWithGlobalFallbackTest.scala" // non-native JS classes - -- "NestedJSClassTest.scala" // non-native JS classes - -- "NonNativeJSTypeTest.scala" // non-native JS classes - -- "PromiseMock.scala" // non-native JS classes + -- "NonNativeJSTypeTest.scala" // 3 tests fail (2 because of anonymous JS class no-own-prototype; 1 because of a progression for value class fields) )).get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/junit" ** (("*.scala": FileFilter) @@ -1072,10 +1060,9 @@ object Build { )).get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/library" ** (("*.scala": FileFilter) - -- "BigIntTest.scala" // non-native JS classes -- "ObjectTest.scala" // compile errors caused by #9588 -- "StackTraceTest.scala" // would require `npm install source-map-support` - -- "UnionTypeTest.scala" // requires a Scala 2 macro + -- "UnionTypeTest.scala" // requires the Scala 2 macro defined in Typechecking*.scala )).get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/niobuffer" ** "*.scala").get @@ -1083,14 +1070,8 @@ object Build { ++ (dir / "js/src/test/scala/org/scalajs/testsuite/typedarray" ** "*.scala").get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/utils" ** "*.scala").get - ++ (dir / "js/src/test/require-2.12" ** (("*.scala": FileFilter) - -- "JSOptionalTest212.scala" // non-native JS classes - )).get - - ++ (dir / "js/src/test/require-sam" ** (("*.scala": FileFilter) - -- "SAMJSTest.scala" // non-native JS classes - )).get - + ++ (dir / "js/src/test/require-2.12" ** "*.scala").get + ++ (dir / "js/src/test/require-sam" ** "*.scala").get ++ (dir / "js/src/test/scala-new-collections" ** "*.scala").get ) } From d31dddffe78c966b5784b567288cc2c3d4589ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 18 Sep 2020 17:44:06 +0200 Subject: [PATCH 02/11] Better implementation for isFullyApplied. --- .../tools/dotc/transform/sjs/ExplicitJSClasses.scala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index 685689d144d5..ac8f42e4583f 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -646,12 +646,8 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => * In this phase, `transformApply`, `transformTypeApply` and `transformSelect` * must only operate on fully applied selections and applications. */ - private def isFullyApplied(tree: Tree)(using Context): Boolean = { - tree.tpe.widenTermRefExpr match { - case _:PolyType | _:MethodType => false - case _ => true - } - } + private def isFullyApplied(tree: Tree)(using Context): Boolean = + !tree.tpe.widenTermRefExpr.isInstanceOf[MethodOrPoly] /** Wraps `super` calls to inner and local JS classes with * `withContextualJSClassValue`, to preserve a reified reference to the From 534a5b207ee31c30999ce542a9621bbb58a33810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 18 Sep 2020 17:53:52 +0200 Subject: [PATCH 03/11] Use pattern matching in transformTemplate. --- .../transform/sjs/ExplicitJSClasses.scala | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index ac8f42e4583f..2d3e693d621f 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -455,41 +455,43 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => if (fixedParents eq tree.parents) tree else cpy.Template(tree)(parents = fixedParents) } else { - val newDecls = List.newBuilder[Tree] - for (decl <- tree.body) { - val declSym = decl.symbol - if (declSym eq null) { - // not a member def, do nothing - } else if (isJSClass(declSym)) { - val jsclassAccessor = jsclassAccessorFor(declSym) - - val rhs = if (cls.hasAnnotation(jsdefn.JSNativeAnnot)) { - ref(jsdefn.JSPackage_native) - } else { - val clazzValue = clsOf(declSym.typeRef) - if (cls.isStaticOwner) { - // #4086 - ref(jsdefn.Runtime_constructorOf).appliedTo(clazzValue) + val newStats = List.newBuilder[Tree] + for (stat <- tree.body) { + stat match { + case stat: TypeDef if stat.isClassDef && isJSClass(stat.symbol) => + val innerClassSym = stat.symbol.asClass + val jsclassAccessor = jsclassAccessorFor(innerClassSym) + + val rhs = if (cls.hasAnnotation(jsdefn.JSNativeAnnot)) { + ref(jsdefn.JSPackage_native) } else { - val parentTpe = extractSuperTpeFromImpl(decl.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]) - val superClassCtor = genJSConstructorOf(tree, parentTpe) - ref(jsdefn.Runtime_createInnerJSClass).appliedTo(clazzValue, superClassCtor) + val clazzValue = clsOf(innerClassSym.typeRef) + if (cls.isStaticOwner) { + // scala-js/scala-js#4086 + ref(jsdefn.Runtime_constructorOf).appliedTo(clazzValue) + } else { + val parentTpe = extractSuperTpeFromImpl(stat.rhs.asInstanceOf[Template]) + val superClassCtor = genJSConstructorOf(tree, parentTpe) + ref(jsdefn.Runtime_createInnerJSClass).appliedTo(clazzValue, superClassCtor) + } } - } - newDecls += ValDef(jsclassAccessor, rhs) - } else if (cls.isStaticOwner) { - // #4086 - if (isExposedModule(declSym)) { - val getter = cls.info.decls.lookup(jsobjectGetterNameFor(declSym)).asTerm - newDecls += DefDef(getter, ref(declSym)) - } + newStats += ValDef(jsclassAccessor, rhs) + + case stat: ValDef if cls.isStaticOwner && isExposedModule(stat.symbol) => + // scala-js/scala-js#4086 + val moduleSym = stat.symbol + val getter = cls.info.decls.lookup(jsobjectGetterNameFor(moduleSym)).asTerm + newStats += DefDef(getter, ref(moduleSym)) + + case _ => + () // nothing to do } - newDecls += decl + newStats += stat } - cpy.Template(tree)(tree.constr, fixedParents, Nil, tree.self, newDecls.result()) + cpy.Template(tree)(tree.constr, fixedParents, Nil, tree.self, newStats.result()) } } From fc524522e40548230f680c2764fa297256e100cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 18 Sep 2020 18:02:07 +0200 Subject: [PATCH 04/11] Implement HashSet.-= in terms of remove, not the other way around. --- compiler/src/dotty/tools/dotc/util/HashSet.scala | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/HashSet.scala b/compiler/src/dotty/tools/dotc/util/HashSet.scala index c856b8b01515..e7406f9ab094 100644 --- a/compiler/src/dotty/tools/dotc/util/HashSet.scala +++ b/compiler/src/dotty/tools/dotc/util/HashSet.scala @@ -102,7 +102,7 @@ class HashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends Mu def +=(x: T): Unit = put(x) - def -= (x: T): Unit = + def remove(x: T): Boolean = Stats.record(statsItem("remove")) var idx = firstIndex(x) var e = entryAt(idx) @@ -124,16 +124,13 @@ class HashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends Mu hole = idx table(hole) = null used -= 1 - return + return true idx = nextIndex(idx) e = entryAt(idx) + false - def remove(x: T): Boolean = - if contains(x) then - this -= x - true - else - false + def -=(x: T): Unit = + remove(x) private def addOld(x: T) = Stats.record(statsItem("re-enter")) From 7dbea352a595ba02dbcd925d5541237103e5f8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 18 Sep 2020 18:52:49 +0200 Subject: [PATCH 05/11] Address my self-review. --- .../src/dotty/tools/backend/sjs/JSCodeGen.scala | 8 +++----- .../dotty/tools/backend/sjs/JSExportsGen.scala | 11 +++++++---- .../src/dotty/tools/dotc/core/TypeErasure.scala | 2 -- .../dotc/transform/sjs/ExplicitJSClasses.scala | 15 ++++++--------- .../tools/dotc/transform/sjs/JSSymUtils.scala | 3 ++- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 10f421c31e2b..2547a340b0a9 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -789,13 +789,11 @@ class JSCodeGen()(using genCtx: Context) { * initialized to an instance of the boxed representation, with * an underlying value set to the zero of its type. However we * cannot implement that, so we live with the discrepancy. - * Anyway, scalac also has problems with uninitialized value - * class values, if they come from a generic context. * - * TODO Evaluate how much of this needs to be adapted for dotc, - * which unboxes `null` to the zero of their underlying. + * In dotc this is usually not an issue, because it unboxes `null` to + * the zero of the underlying type, unlike scalac which throws an NPE. */ - jstpe.ClassType(encodeClassName(tpe.valueClassSymbol)) + jstpe.ClassType(encodeClassName(tpe.tycon.typeSymbol)) case _ => // Other types are not boxed, so we can initialized them to their true zero. diff --git a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala index baae107aa09a..1ae27a3e10e6 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala @@ -652,6 +652,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { val isLiftedJSConstructor = sym.isClassConstructor && sym.owner.isNestedJSClass + // params: List[ParamSpec] ; captureParams and captureParamsBack: List[js.ParamDef] val (params, captureParamsFront, captureParamsBack) = { val paramNamessNow = sym.info.paramNamess val paramInfosNow = sym.info.paramInfoss.flatten @@ -684,9 +685,10 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { * by explicitouter, while the other capture params are added by * lambdalift (between elimErasedValueType and now). * - * scalac: Note that lambdalift creates new symbols even for parameters that - * are not the result of lambda lifting, but it preserves their - * `name`s. + * Since we cannot reliably get Symbols for parameters created by + * intermediate phases, we have to perform some dance with the + * paramNamess and paramInfoss observed at some phases, combined with + * the paramSymss observed at elimRepeated. */ val hasOuterParam = { @@ -724,7 +726,8 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { // For an anonymous JS class constructor, we put the capture parameters back as formal parameters. val allFormalParams = captureParamsFront.toIndexedSeq ++ formalParams ++ captureParamsBack.toIndexedSeq (allFormalParams, Nil, Nil) - } else {*/ (formalParams, captureParamsFront, captureParamsBack) + } else {*/ + (formalParams, captureParamsFront, captureParamsBack) //} } } diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index d363228f2d2f..e8726f9eb73b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -95,8 +95,6 @@ object TypeErasure { abstract case class ErasedValueType(tycon: TypeRef, erasedUnderlying: Type) extends CachedGroundType with ValueType { override def computeHash(bs: Hashable.Binders): Int = doHash(bs, tycon, erasedUnderlying) - - final def valueClassSymbol(using Context): ClassSymbol = tycon.typeSymbol.asClass } final class CachedErasedValueType(tycon: TypeRef, erasedUnderlying: Type) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index 2d3e693d621f..25e3cc7dc65a 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -250,7 +250,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => override def changesMembers: Boolean = true // the phase adds fields for inner JS classes - /** Is the given symbol an owner for which this transformation applies? + /** Is the given symbol an owner that might receive `\$jsclass` and/or `\$jsobject` fields? * * This applies if either or both of the following are true: * - It is not a static owner, or @@ -258,12 +258,9 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => * * The latter is necessary for scala-js/scala-js#4086. */ - private def isApplicableOwner(sym: Symbol)(using Context): Boolean = { - !sym.isStaticOwner || ( - sym.is(ModuleClass) && - sym.hasAnnotation(jsdefn.JSTypeAnnot) && - !sym.hasAnnotation(jsdefn.JSNativeAnnot) - ) + private def mayNeedJSClassOrJSObjectFields(sym: Symbol)(using Context): Boolean = { + !sym.isStaticOwner + || (sym.is(ModuleClass) && sym.hasAnnotation(jsdefn.JSTypeAnnot) && !sym.hasAnnotation(jsdefn.JSNativeAnnot)) } /** Is the given symbol a JS class (that is not a trait nor an object)? */ @@ -329,7 +326,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => } override def transformInfo(tp: Type, sym: Symbol)(using Context): Type = tp match { - case tp @ ClassInfo(_, cls, _, decls, _) if !cls.is(JavaDefined) && isApplicableOwner(cls) => + case tp @ ClassInfo(_, cls, _, decls, _) if !cls.is(JavaDefined) && mayNeedJSClassOrJSObjectFields(cls) => val innerJSClasses = decls.filter(isJSClass) val innerObjectsForAdHocExposed = @@ -451,7 +448,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => if (!cls.isJSType) tree.parents // fast path else tree.parents.mapConserve(unwrapWithContextualJSClassValue(_)) - if (!isApplicableOwner(cls)) { + if (!mayNeedJSClassOrJSObjectFields(cls)) { if (fixedParents eq tree.parents) tree else cpy.Template(tree)(parents = fixedParents) } else { diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala index b769b77b6d5b..f9bee18f894a 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala @@ -53,8 +53,9 @@ object JSSymUtils { def isNonNativeJSClass(using Context): Boolean = sym.isJSType && !sym.hasAnnotation(jsdefn.JSNativeAnnot) + /** Is this symbol a nested JS class, i.e., an inner or local JS class? */ def isNestedJSClass(using Context): Boolean = - !sym.isStatic /*&& !isStaticModule(sym.originalOwner)*/ && sym.isJSType + !sym.isStatic && sym.isJSType /** Tests whether the given member is exposed, i.e., whether it was * originally a public or protected member of a non-native JS class. From 7c645b0f03d9a31c59e54f1293c7f8ad83571d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 19 Sep 2020 12:08:18 +0200 Subject: [PATCH 06/11] Implement the behavior of anonymous JS classes. This was in fact much easier than I thought. Unlike in scalac, where we forcefully reattach captures as normal constructor arguments, here we keep them as captures. This is much more natural, and yields a simpler implementation. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 296 ++++++++++++++++-- .../tools/backend/sjs/JSDefinitions.scala | 2 + .../dotty/tools/backend/sjs/JSEncoding.scala | 17 +- .../tools/backend/sjs/JSExportsGen.scala | 24 +- .../dotty/tools/backend/sjs/ScopedVar.scala | 2 + .../transform/sjs/ExplicitJSClasses.scala | 25 +- project/Build.scala | 2 +- 7 files changed, 309 insertions(+), 59 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 2547a340b0a9..50c2fec03954 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -75,6 +75,7 @@ class JSCodeGen()(using genCtx: Context) { // Some state -------------------------------------------------------------- + private val lazilyGeneratedAnonClasses = new MutableSymbolMap[TypeDef] private val generatedClasses = mutable.ListBuffer.empty[js.ClassDef] private val generatedStaticForwarderClasses = mutable.ListBuffer.empty[(Symbol, js.ClassDef)] @@ -82,11 +83,26 @@ class JSCodeGen()(using genCtx: Context) { private val currentMethodSym = new ScopedVar[Symbol] private val localNames = new ScopedVar[LocalNameGenerator] private val thisLocalVarIdent = new ScopedVar[Option[js.LocalIdent]] + private val isModuleInitialized = new ScopedVar[ScopedVar.VarBox[Boolean]] private val undefinedDefaultParams = new ScopedVar[mutable.Set[Symbol]] /* Contextual JS class value for some operations of nested JS classes that need one. */ private val contextualJSClassValue = new ScopedVar[Option[js.Tree]](None) + /** Resets all of the scoped state in the context of `body`. */ + private def resetAllScopedVars[T](body: => T): T = { + withScopedVars( + currentClassSym := null, + currentMethodSym := null, + localNames := null, + thisLocalVarIdent := null, + isModuleInitialized := null, + undefinedDefaultParams := null + ) { + body + } + } + private def acquireContextualJSClassValue[A](f: Option[js.Tree] => A): A = { val jsClassValue = contextualJSClassValue.get withScopedVars( @@ -105,11 +121,6 @@ class JSCodeGen()(using genCtx: Context) { /** Implicitly materializes the current local name generator. */ implicit def implicitLocalNames: LocalNameGenerator = localNames.get - /* See genSuperCall() - * TODO Can we avoid this unscoped var? - */ - private var isModuleInitialized: Boolean = false - private def currentClassType = encodeClassType(currentClassSym) /** Returns a new fresh local identifier. */ @@ -124,6 +135,16 @@ class JSCodeGen()(using genCtx: Context) { private def freshLocalIdent(base: TermName)(implicit pos: Position): js.LocalIdent = localNames.get.freshLocalIdent(base) + private def consumeLazilyGeneratedAnonClass(sym: Symbol): TypeDef = { + val typeDef = lazilyGeneratedAnonClasses.remove(sym) + if (typeDef == null) { + throw new FatalError( + i"Could not find tree for lazily generated anonymous class ${sym.fullName} at ${sym.sourcePos}") + } else { + typeDef + } + } + // Compilation unit -------------------------------------------------------- def run(): Unit = { @@ -165,10 +186,15 @@ class JSCodeGen()(using genCtx: Context) { } val allTypeDefs = collectTypeDefs(cunit.tpdTree) - // TODO Record anonymous JS function classes + val (anonJSClassTypeDefs, otherTypeDefs) = + allTypeDefs.partition(td => td.symbol.isAnonymousClass && td.symbol.isJSType) + + // Record the TypeDefs of anonymous JS classes to be lazily generated + for (td <- anonJSClassTypeDefs) + lazilyGeneratedAnonClasses(td.symbol) = td /* Finally, we emit true code for the remaining class defs. */ - for (td <- allTypeDefs) { + for (td <- otherTypeDefs) { val sym = td.symbol implicit val pos: Position = sym.span @@ -181,8 +207,6 @@ class JSCodeGen()(using genCtx: Context) { currentClassSym := sym ) { val tree = if (isJSType(sym)) { - /*assert(!isRawJSFunctionDef(sym), - s"Raw JS function def should have been recorded: $cd")*/ if (!sym.is(Trait) && sym.isNonNativeJSClass) genNonNativeJSClass(td) else @@ -915,8 +939,8 @@ class JSCodeGen()(using genCtx: Context) { val (captureParams, dispatch) = jsExportsGen.genJSConstructorDispatch(constructorTrees.map(_.symbol)) - /* Ensure that the first JS class capture is a reference to the JS - * super class value. genNonNativeJSClass relies on this. + /* Ensure that the first JS class capture is a reference to the JS super class value. + * genNonNativeJSClass and genNewAnonJSClass rely on this. */ val captureParamsWithJSSuperClass = captureParams.map { params => val jsSuperClassParam = js.ParamDef( @@ -974,12 +998,11 @@ class JSCodeGen()(using genCtx: Context) { val vparamss = dd.vparamss val rhs = dd.rhs - isModuleInitialized = false - withScopedVars( currentMethodSym := sym, undefinedDefaultParams := mutable.Set.empty, - thisLocalVarIdent := None + thisLocalVarIdent := None, + isModuleInitialized := new ScopedVar.VarBox(false) ) { assert(vparamss.isEmpty || vparamss.tail.isEmpty, "Malformed parameter list: " + vparamss) @@ -1666,9 +1689,9 @@ class JSCodeGen()(using genCtx: Context) { genExpr(qual), sym, genActualArgs(sym, args)) // Initialize the module instance just after the super constructor call. - if (isStaticModule(currentClassSym) && !isModuleInitialized && + if (isStaticModule(currentClassSym) && !isModuleInitialized.get.value && currentMethodSym.get.isClassConstructor) { - isModuleInitialized = true + isModuleInitialized.get.value = true val className = encodeClassName(currentClassSym) val thisType = jstpe.ClassType(className) val initModule = js.StoreModule(className, js.This()(thisType)) @@ -1755,8 +1778,8 @@ class JSCodeGen()(using genCtx: Context) { js.JSObjectConstr(Nil) else if (cls == jsdefn.JSArrayClass && args.isEmpty) js.JSArrayConstr(Nil) - //else if (cls.isAnonymousClass) - // genAnonJSClassNew(cls, jsClassValue.get, genArgs)(fun.pos) + else if (cls.isAnonymousClass) + genNewAnonJSClass(cls, jsClassValue.get, args.map(genExpr))(fun.span) else if (!nestedJSClass) js.JSNew(genLoadJSConstructor(cls), genArgs) else if (!atPhase(erasurePhase)(cls.is(ModuleClass))) // LambdaLift removes the ModuleClass flag of lifted classes @@ -1772,6 +1795,228 @@ class JSCodeGen()(using genCtx: Context) { js.JSNew(js.CreateJSClass(encodeClassName(sym), jsSuperClassValue :: args), Nil) } + /** Generate an instance of an anonymous (non-lambda) JS class inline + * + * @param sym Class to generate the instance of + * @param jsSuperClassValue JS class value of the super class + * @param args Arguments to the Scala constructor, which map to JS class captures + * @param pos Position of the original New tree + */ + private def genNewAnonJSClass(sym: Symbol, jsSuperClassValue: js.Tree, args: List[js.Tree])( + implicit pos: Position): js.Tree = { + assert(sym.isAnonymousClass, + s"Generating AnonJSClassNew of non anonymous JS class ${sym.fullName}") + + // Find the TypeDef for this anonymous class and generate it + val typeDef = consumeLazilyGeneratedAnonClass(sym) + val originalClassDef = resetAllScopedVars { + withScopedVars( + currentClassSym := sym + ) { + genNonNativeJSClass(typeDef) + } + } + + // Partition class members. + val privateFieldDefs = mutable.ListBuffer.empty[js.FieldDef] + val classDefMembers = mutable.ListBuffer.empty[js.MemberDef] + val instanceMembers = mutable.ListBuffer.empty[js.MemberDef] + var constructor: Option[js.JSMethodDef] = None + + originalClassDef.memberDefs.foreach { + case fdef: js.FieldDef => + privateFieldDefs += fdef + + case fdef: js.JSFieldDef => + instanceMembers += fdef + + case mdef: js.MethodDef => + assert(mdef.flags.namespace.isStatic, + "Non-static, unexported method in non-native JS class") + classDefMembers += mdef + + case mdef: js.JSMethodDef => + mdef.name match { + case js.StringLiteral("constructor") => + assert(!mdef.flags.namespace.isStatic, "Exported static method") + assert(constructor.isEmpty, "two ctors in class") + constructor = Some(mdef) + + case _ => + assert(!mdef.flags.namespace.isStatic, "Exported static method") + instanceMembers += mdef + } + + case property: js.JSPropertyDef => + instanceMembers += property + + case nativeMemberDef: js.JSNativeMemberDef => + throw new FatalError("illegal native JS member in JS class at " + nativeMemberDef.pos) + } + + assert(originalClassDef.topLevelExportDefs.isEmpty, + "Found top-level exports in anonymous JS class at " + pos) + + // Make new class def with static members + val newClassDef = { + implicit val pos = originalClassDef.pos + val parent = js.ClassIdent(jsNames.ObjectClass) + js.ClassDef(originalClassDef.name, originalClassDef.originalName, + ClassKind.AbstractJSType, None, Some(parent), interfaces = Nil, + jsSuperClass = None, jsNativeLoadSpec = None, + classDefMembers.toList, Nil)( + originalClassDef.optimizerHints) + } + + generatedClasses += newClassDef + + // Construct inline class definition + + val jsClassCaptures = originalClassDef.jsClassCaptures.getOrElse { + throw new AssertionError(s"no class captures for anonymous JS class at $pos") + } + val js.JSMethodDef(_, _, ctorParams, ctorBody) = constructor.getOrElse { + throw new AssertionError("No ctor found") + } + assert(ctorParams.isEmpty, s"non-empty constructor params for anonymous JS class at $pos") + + /* The first class capture is always a reference to the super class. + * This is enforced by genJSClassCapturesAndConstructor. + */ + def jsSuperClassRef(implicit pos: ir.Position): js.VarRef = + jsClassCaptures.head.ref + + /* The `this` reference. + * FIXME This could clash with a local variable of the constructor or a JS + * class capture. It seems Scala 2 has the same vulnerability. How do we + * avoid this? + */ + val selfName = freshLocalIdent("this")(pos) + def selfRef(implicit pos: ir.Position) = + js.VarRef(selfName)(jstpe.AnyType) + + def memberLambda(params: List[js.ParamDef], body: js.Tree)(implicit pos: ir.Position): js.Closure = + js.Closure(arrow = false, captureParams = Nil, params, body, captureValues = Nil) + + val memberDefinitions0 = instanceMembers.toList.map { + case fdef: js.FieldDef => + throw new AssertionError("unexpected FieldDef") + + case fdef: js.JSFieldDef => + implicit val pos = fdef.pos + js.Assign(js.JSSelect(selfRef, fdef.name), jstpe.zeroOf(fdef.ftpe)) + + case mdef: js.MethodDef => + throw new AssertionError("unexpected MethodDef") + + case mdef: js.JSMethodDef => + implicit val pos = mdef.pos + val impl = memberLambda(mdef.args, mdef.body) + js.Assign(js.JSSelect(selfRef, mdef.name), impl) + + case pdef: js.JSPropertyDef => + implicit val pos = pdef.pos + val optGetter = pdef.getterBody.map { body => + js.StringLiteral("get") -> memberLambda(params = Nil, body) + } + val optSetter = pdef.setterArgAndBody.map { case (arg, body) => + js.StringLiteral("set") -> memberLambda(params = arg :: Nil, body) + } + val descriptor = js.JSObjectConstr( + optGetter.toList ::: + optSetter.toList ::: + List(js.StringLiteral("configurable") -> js.BooleanLiteral(true)) + ) + js.JSMethodApply(js.JSGlobalRef("Object"), + js.StringLiteral("defineProperty"), + List(selfRef, pdef.name, descriptor)) + + case nativeMemberDef: js.JSNativeMemberDef => + throw new FatalError("illegal native JS member in JS class at " + nativeMemberDef.pos) + } + + val memberDefinitions = if (privateFieldDefs.isEmpty) { + memberDefinitions0 + } else { + /* Private fields, declared in FieldDefs, are stored in a separate + * object, itself stored as a non-enumerable field of the `selfRef`. + * The name of that field is retrieved at + * `scala.scalajs.runtime.privateFieldsSymbol()`, and is a Symbol if + * supported, or a randomly generated string that has the same enthropy + * as a UUID (i.e., 128 random bits). + * + * This encoding solves two issues: + * + * - Hide private fields in anonymous JS classes from `JSON.stringify` + * and other cursory inspections in JS (#2748). + * - Get around the fact that abstract JS types cannot declare + * FieldDefs (#3777). + */ + val fieldsObjValue = { + js.JSObjectConstr(privateFieldDefs.toList.map { fdef => + implicit val pos = fdef.pos + js.StringLiteral(fdef.name.name.nameString) -> jstpe.zeroOf(fdef.ftpe) + }) + } + val definePrivateFieldsObj = { + /* Object.defineProperty(selfRef, privateFieldsSymbol, { + * value: fieldsObjValue + * }); + * + * `writable`, `configurable` and `enumerable` are false by default. + */ + js.JSMethodApply( + js.JSGlobalRef("Object"), + js.StringLiteral("defineProperty"), + List( + selfRef, + genPrivateFieldsSymbol()(using sym.sourcePos), + js.JSObjectConstr(List( + js.StringLiteral("value") -> fieldsObjValue + )) + ) + ) + } + definePrivateFieldsObj :: memberDefinitions0 + } + + // Transform the constructor body. + val inlinedCtorStats = new ir.Transformers.Transformer { + override def transform(tree: js.Tree, isStat: Boolean): js.Tree = tree match { + // The super constructor call. Transform this into a simple new call. + case js.JSSuperConstructorCall(args) => + implicit val pos = tree.pos + + val newTree = { + val ident = originalClassDef.superClass.getOrElse(throw new FatalError("No superclass")) + if (args.isEmpty && ident.name == JSObjectClassName) + js.JSObjectConstr(Nil) + else + js.JSNew(jsSuperClassRef, args) + } + + js.Block( + js.VarDef(selfName, thisOriginalName, jstpe.AnyType, mutable = false, newTree) :: + memberDefinitions) + + case js.This() => + selfRef(tree.pos) + + // Don't traverse closure boundaries + case closure: js.Closure => + val newCaptureValues = closure.captureValues.map(transformExpr) + closure.copy(captureValues = newCaptureValues)(closure.pos) + + case tree => + super.transform(tree, isStat) + } + }.transform(ctorBody, isStat = true) + + val closure = js.Closure(arrow = true, jsClassCaptures, Nil, + js.Block(inlinedCtorStats, selfRef), jsSuperClassValue :: args) + js.JSFunctionApply(closure, Nil) + } + /** Gen JS code for a primitive method call. */ private def genPrimitiveOp(tree: Apply, isStat: Boolean): js.Tree = { import dotty.tools.backend.ScalaPrimitivesOps._ @@ -3436,9 +3681,6 @@ class JSCodeGen()(using genCtx: Context) { def paramNamesAndTypes(using Context): List[(Names.TermName, Type)] = sym.info.paramNamess.flatten.zip(sym.info.paramInfoss.flatten) - /*val isAnonJSClassConstructor = - sym.isClassConstructor && sym.owner.isAnonymousClass*/ - val wereRepeated = atPhase(elimRepeatedPhase) { val list = for ((name, tpe) <- paramNamesAndTypes) @@ -3454,9 +3696,7 @@ class JSCodeGen()(using genCtx: Context) { val argsParamNamesAndTypes = args.zip(paramNamesAndTypes) for ((arg, (paramName, paramType)) <- argsParamNamesAndTypes) { - val wasRepeated = - /*if (isAnonJSClassConstructor) Some(false) - else*/ wereRepeated.get(paramName) + val wasRepeated = wereRepeated.get(paramName) wasRepeated match { case Some(true) => @@ -3616,11 +3856,11 @@ class JSCodeGen()(using genCtx: Context) { if (sym.owner.isNonNativeJSClass) { val f = if (sym.isJSExposed) { js.JSSelect(qual, genExpr(sym.jsName)) - } else /*if (sym.owner.isAnonymousClass) { + } else if (sym.owner.isAnonymousClass) { js.JSSelect( js.JSSelect(qual, genPrivateFieldsSymbol()), encodeFieldSymAsStringLiteral(sym)) - } else*/ { + } else { js.JSPrivateSelect(qual, encodeClassName(sym.owner), encodeFieldSym(sym)) } @@ -3666,6 +3906,10 @@ class JSCodeGen()(using genCtx: Context) { } } + /** Generates a call to `runtime.privateFieldsSymbol()` */ + private def genPrivateFieldsSymbol()(implicit pos: SourcePosition): js.Tree = + genModuleApplyMethod(jsdefn.Runtime_privateFieldsSymbol, Nil) + /** Generate loading of a module value. * * Can be given either the module symbol or its module class symbol. diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index dd0471921a4c..b4b2d038bc0e 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -153,6 +153,8 @@ final class JSDefinitions()(using Context) { def Runtime_toScalaVarArgs(using Context) = Runtime_toScalaVarArgsR.symbol @threadUnsafe lazy val Runtime_toJSVarArgsR = RuntimePackageClass.requiredMethodRef("toJSVarArgs") def Runtime_toJSVarArgs(using Context) = Runtime_toJSVarArgsR.symbol + @threadUnsafe lazy val Runtime_privateFieldsSymbolR = RuntimePackageClass.requiredMethodRef("privateFieldsSymbol") + def Runtime_privateFieldsSymbol(using Context) = Runtime_privateFieldsSymbolR.symbol @threadUnsafe lazy val Runtime_constructorOfR = RuntimePackageClass.requiredMethodRef("constructorOf") def Runtime_constructorOf(using Context) = Runtime_constructorOfR.symbol @threadUnsafe lazy val Runtime_newConstructorTagR = RuntimePackageClass.requiredMethodRef("newConstructorTag") diff --git a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala index 8efa0d35af31..02e660bbdd8b 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala @@ -176,16 +176,19 @@ object JSEncoding { js.LabelIdent(localNames.labelSymbolName(sym)) } - def encodeFieldSym(sym: Symbol)( - implicit ctx: Context, pos: ir.Position): js.FieldIdent = { - require(sym.owner.isClass && sym.isTerm && !sym.is(Flags.Method) && !sym.is(Flags.Module), + def encodeFieldSym(sym: Symbol)(implicit ctx: Context, pos: ir.Position): js.FieldIdent = + js.FieldIdent(FieldName(encodeFieldSymAsString(sym))) + + def encodeFieldSymAsStringLiteral(sym: Symbol)(implicit ctx: Context, pos: ir.Position): js.StringLiteral = + js.StringLiteral(encodeFieldSymAsString(sym)) + + private def encodeFieldSymAsString(sym: Symbol)(using Context): String = { + require(sym.owner.isClass && sym.isTerm && !sym.isOneOf(Method | Module), "encodeFieldSym called with non-field symbol: " + sym) val name0 = sym.javaSimpleName - val name = - if (name0.charAt(name0.length()-1) != ' ') name0 - else name0.substring(0, name0.length()-1) - js.FieldIdent(FieldName(name)) + if (name0.charAt(name0.length() - 1) != ' ') name0 + else name0.substring(0, name0.length() - 1) } def encodeMethodSym(sym: Symbol, reflProxy: Boolean = false)( diff --git a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala index 1ae27a3e10e6..9dda34d27008 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala @@ -88,10 +88,10 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { def genJSConstructorDispatch(alts: List[Symbol]): (Option[List[js.ParamDef]], js.JSMethodDef) = { val exporteds = alts.map(Exported) - val isLiftedJSCtor = exporteds.head.isLiftedJSConstructor - assert(exporteds.tail.forall(_.isLiftedJSConstructor == isLiftedJSCtor), - s"Alternative constructors $alts do not agree on whether they are lifted JS constructors or not") - val captureParams = if (!isLiftedJSCtor) { + val isConstructorOfNestedJSClass = exporteds.head.isConstructorOfNestedJSClass + assert(exporteds.tail.forall(_.isConstructorOfNestedJSClass == isConstructorOfNestedJSClass), + s"Alternative constructors $alts do not agree on whether they are in a nested JS class or not") + val captureParams = if (!isConstructorOfNestedJSClass) { None } else { Some(for { @@ -645,11 +645,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { // This is a case class because we rely on its structural equality private final case class Exported(sym: Symbol) { - private val isAnonJSClassConstructor = - //sym.isClassConstructor && sym.owner.isAnonymousClass && isJSType(sym.owner) - false - - val isLiftedJSConstructor = + val isConstructorOfNestedJSClass = sym.isClassConstructor && sym.owner.isNestedJSClass // params: List[ParamSpec] ; captureParams and captureParamsBack: List[js.ParamDef] @@ -672,7 +668,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { }).toIndexedSeq } - if (!isLiftedJSConstructor && !isAnonJSClassConstructor) { + if (!isConstructorOfNestedJSClass) { // Easy case: all params are formal params. assert(paramInfosAtElimRepeated.size == paramInfosAtElimEVT.size, s"Found ${paramInfosAtElimRepeated.size} params entering elimRepeated but " + @@ -722,13 +718,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { val captureParamsFront = captureParamsFrontNow.map(makeCaptureParamDef(_)) val captureParamsBack = captureParamsBackNow.map(makeCaptureParamDef(_)) - /*if (isAnonJSClassConstructor) { - // For an anonymous JS class constructor, we put the capture parameters back as formal parameters. - val allFormalParams = captureParamsFront.toIndexedSeq ++ formalParams ++ captureParamsBack.toIndexedSeq - (allFormalParams, Nil, Nil) - } else {*/ - (formalParams, captureParamsFront, captureParamsBack) - //} + (formalParams, captureParamsFront, captureParamsBack) } } diff --git a/compiler/src/dotty/tools/backend/sjs/ScopedVar.scala b/compiler/src/dotty/tools/backend/sjs/ScopedVar.scala index f8185697e15d..4ce4030fb440 100644 --- a/compiler/src/dotty/tools/backend/sjs/ScopedVar.scala +++ b/compiler/src/dotty/tools/backend/sjs/ScopedVar.scala @@ -35,4 +35,6 @@ object ScopedVar { try body finally stack.reverse.foreach(_.pop()) } + + final class VarBox[A](var value: A) } diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index 25e3cc7dc65a..367cc4ef1179 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -203,9 +203,9 @@ import JSSymUtils._ * so that the back-end receives a reified reference to the parent class of * `O`. * - * TODO A similar treatment is applied on anonymous JS classes, which - * basically define something very similar to an `object`, although without - * its own JS class. + * A similar treatment is applied on anonymous JS classes, which basically + * define something very similar to an `object`, although without their own JS + * class. * * -------------------------------------- * @@ -280,11 +280,11 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => /** Is the given clazz an inner JS class? */ private def isInnerJSClass(clazz: Symbol)(using Context): Boolean = - isInnerJSClassOrObject(clazz) && !clazz.is(ModuleClass) + isInnerJSClassOrObject(clazz) && !isConsideredAnObject(clazz) /** Is the given clazz a local JS class? */ private def isLocalJSClass(clazz: Symbol)(using Context): Boolean = - isLocalJSClassOrObject(clazz) && !clazz.is(ModuleClass) //&& !clazz.isAnonymousClass + isLocalJSClassOrObject(clazz) && !isConsideredAnObject(clazz) /** Is the gen clazz an inner or local JS class or object? */ private def isInnerOrLocalJSClassOrObject(sym: Symbol)(using Context): Boolean = @@ -302,6 +302,16 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => private def isLocalJSClassOrObject(clazz: Symbol)(using Context): Boolean = clazz.isLocalToBlock && !clazz.is(Trait) && clazz.hasAnnotation(jsdefn.JSTypeAnnot) + /** Is the given clazz an inner or local JS object? */ + private def isInnerOrLocalJSObject(clazz: Symbol)(using Context): Boolean = + isInnerOrLocalJSClassOrObject(clazz) && isConsideredAnObject(clazz) + + /** Is the given clazz considered to be an object for the purposes of this phase? + * This is true for module classes and for anonymous JS classes. + */ + private def isConsideredAnObject(clazz: Symbol)(using Context): Boolean = + clazz.is(ModuleClass) || clazz.isAnonymousClass + private def jsclassFieldName(clazzName: TypeName): TermName = clazzName.toTermName ++ "$jsname" @@ -416,7 +426,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => private def populateNestedObject2superClassTpe(stats: List[Tree])(using Context): Unit = { for (stat <- stats) { stat match { - case cd @ TypeDef(_, rhs) if cd.isClassDef && cd.symbol.is(ModuleClass) && isInnerOrLocalJSClassOrObject(cd.symbol) => + case cd @ TypeDef(_, rhs) if cd.isClassDef && isInnerOrLocalJSObject(cd.symbol) => myState.nestedObject2superClassTpe(cd.symbol) = extractSuperTpeFromImpl(rhs.asInstanceOf[Template]) case _ => } @@ -575,11 +585,10 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => * `withContextualJSClassValue`, to preserve a reified reference to * the necessary JS class value (the class itself for classes, or the * super class for objects). - * Anonymous classes are considered as "objects" for this purpose. */ val cls = sym.owner if (isInnerOrLocalJSClassOrObject(cls)) { - if (!cls.is(ModuleClass) /*&& !cls.isAnonymousClass*/) { + if (!isConsideredAnObject(cls)) { methPart(tree) match { case Select(n @ New(tpt), _) => val jsclassValue = genJSConstructorOf(tpt, n.tpe) diff --git a/project/Build.scala b/project/Build.scala index 93a8bbb4099d..7f518f87688d 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1048,7 +1048,7 @@ object Build { -- "DynamicTest.scala" // one test requires JS exports, all other tests pass -- "ExportsTest.scala" // JS exports -- "JSExportStaticTest.scala" // JS exports - -- "NonNativeJSTypeTest.scala" // 3 tests fail (2 because of anonymous JS class no-own-prototype; 1 because of a progression for value class fields) + -- "NonNativeJSTypeTest.scala" // 1 test fails because of a progression for value class fields (needs an update upstream) )).get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/junit" ** (("*.scala": FileFilter) From bd03e71e11f2a198a7c5e81125c2a35c1ad87679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 19 Sep 2020 12:18:47 +0200 Subject: [PATCH 07/11] Better extraction of the super type constructor. --- .../transform/sjs/ExplicitJSClasses.scala | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index 367cc4ef1179..22aca7f7afda 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -419,7 +419,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => override def prepareForUnit(tree: Tree)(using Context): Context = ctx.fresh.updateStore(MyState, new MyState()) - /** Populate `nestedObject2superClassTpe` for inner objects at the start of + /** Populate `nestedObject2superTypeConstructor` for inner objects at the start of * a `Block` or `Template`, so that they are visible even before their * definition (in their enclosing scope). */ @@ -427,7 +427,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => for (stat <- stats) { stat match { case cd @ TypeDef(_, rhs) if cd.isClassDef && isInnerOrLocalJSObject(cd.symbol) => - myState.nestedObject2superClassTpe(cd.symbol) = extractSuperTpeFromImpl(rhs.asInstanceOf[Template]) + myState.nestedObject2superTypeConstructor(cd.symbol) = extractSuperTypeConstructor(rhs) case _ => } } @@ -477,7 +477,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => // scala-js/scala-js#4086 ref(jsdefn.Runtime_constructorOf).appliedTo(clazzValue) } else { - val parentTpe = extractSuperTpeFromImpl(stat.rhs.asInstanceOf[Template]) + val parentTpe = extractSuperTypeConstructor(stat.rhs) val superClassCtor = genJSConstructorOf(tree, parentTpe) ref(jsdefn.Runtime_createInnerJSClass).appliedTo(clazzValue, superClassCtor) } @@ -525,7 +525,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => val rhs = { val typeRef = tree.tpe val clazzValue = clsOf(typeRef) - val superClassCtor = genJSConstructorOf(tree, extractSuperTpeFromImpl(tree.rhs.asInstanceOf[Template])) + val superClassCtor = genJSConstructorOf(tree, extractSuperTypeConstructor(tree.rhs)) val fakeNewInstances = { /* We need to use `reverse` because the Scope returns elements in reverse order compared to tree definitions. * The back-end needs the fake News to be in the same order as the corresponding tree definitions. @@ -598,7 +598,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => tree } } else { - wrapWithContextualJSClassValue(myState.nestedObject2superClassTpe(cls))(tree) + wrapWithContextualJSClassValue(myState.nestedObject2superTypeConstructor(cls))(tree) } } else { tree @@ -727,12 +727,20 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => /** Extracts the super type constructor of a `Template`, without type * parameters, so that the type is well-formed outside of the `Template`, * i.e., at the same level where the corresponding `TypeDef` is defined. - * It is not necessarily *-kinded, though, which limits its applicability. + * + * For example, for the Template of a class definition like + * {{{ + * class Foo[...Ts] extends pre.Parent[...Us](...args) with ... { ... } + * }}} + * we extract the type constructor `pre.Parent`, without its type + * parameters. + * + * Since the result is not necessarily *-kinded, its applicability is + * limited. It seems to be sufficient to put in a `classOf`, though, which + * is what we care about. */ - private def extractSuperTpeFromImpl(impl: Template)(using Context): Type = { - // TODO Check whether stripPoly is the right thing. Do we need a sort of rawTypeRef? - impl.parents.head.tpe.stripPoly - } + private def extractSuperTypeConstructor(typeDefRhs: Tree)(using Context): Type = + typeDefRhs.asInstanceOf[Template].parents.head.tpe.dealias.typeConstructor } object ExplicitJSClasses { @@ -741,7 +749,7 @@ object ExplicitJSClasses { val LocalJSClassValueName: UniqueNameKind = new UniqueNameKind("$jsclass") private final class MyState { - val nestedObject2superClassTpe = new MutableSymbolMap[Type] + val nestedObject2superTypeConstructor = new MutableSymbolMap[Type] val localClass2jsclassVal = new MutableSymbolMap[TermSymbol] val notYetSelfReferencingLocalClasses = new util.HashSet[Symbol] } From 046f01fa622434e66b875b852b984c8957b93923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 19 Sep 2020 13:55:32 +0200 Subject: [PATCH 08/11] Make sure that tests that compile and link keep doing so. --- project/Build.scala | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 7f518f87688d..1b1955d607f3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1024,6 +1024,7 @@ object Build { (dir ** filter).get }, + // A first blacklist of tests for those that do not compile or do not link managedSources in Test ++= { val dir = fetchScalaJSSource.value / "test-suite" ( @@ -1037,27 +1038,16 @@ object Build { ++ (dir / "shared/src/test/require-jdk7" ** "*.scala").get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/compiler" ** (("*.scala": FileFilter) - -- "InteroperabilityTest.scala" // 3 tests require JS exports, all other tests pass -- "RuntimeTypesTest.scala" // compile errors: no ClassTag for Null and Nothing )).get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/javalib" ** "*.scala").get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/jsinterop" ** (("*.scala": FileFilter) - -- "AsyncTest.scala" // needs JS exports in PromiseMock.scala - -- "DynamicTest.scala" // one test requires JS exports, all other tests pass - -- "ExportsTest.scala" // JS exports - -- "JSExportStaticTest.scala" // JS exports - -- "NonNativeJSTypeTest.scala" // 1 test fails because of a progression for value class fields (needs an update upstream) + -- "ExportsTest.scala" // JS exports + do not compile because of a var in a structural type )).get - ++ (dir / "js/src/test/scala/org/scalajs/testsuite/junit" ** (("*.scala": FileFilter) - // Tests fail - -- "JUnitAbstractClassTest.scala" - -- "JUnitNamesTest.scala" - -- "JUnitSubClassTest.scala" - -- "MultiCompilationSecondUnitTest.scala" - )).get + ++ (dir / "js/src/test/scala/org/scalajs/testsuite/junit" ** "*.scala").get ++ (dir / "js/src/test/scala/org/scalajs/testsuite/library" ** (("*.scala": FileFilter) -- "ObjectTest.scala" // compile errors caused by #9588 @@ -1074,6 +1064,25 @@ object Build { ++ (dir / "js/src/test/require-sam" ** "*.scala").get ++ (dir / "js/src/test/scala-new-collections" ** "*.scala").get ) + }, + + // A second blacklist for tests that compile and link, but do not pass at run-time. + // Putting them here instead of above makes sure that we do not regress on compilation+linking. + Test / testOptions += Tests.Filter { name => + !Set[String]( + "org.scalajs.testsuite.compiler.InteroperabilityTest", // 3 tests require JS exports, all other tests pass + + "org.scalajs.testsuite.jsinterop.AsyncTest", // needs JS exports in PromiseMock.scala + "org.scalajs.testsuite.jsinterop.DynamicTest", // one test requires JS exports, all other tests pass + "org.scalajs.testsuite.jsinterop.JSExportStaticTest", // JS exports + "org.scalajs.testsuite.jsinterop.NonNativeJSTypeTest", // 1 test fails because of a progression for value class fields (needs an update upstream) + + // Not investigated so far + "org.scalajs.testsuite.junit.JUnitAbstractClassTestCheck", + "org.scalajs.testsuite.junit.JUnitNamesTestCheck", + "org.scalajs.testsuite.junit.JUnitSubClassTestCheck", + "org.scalajs.testsuite.junit.MultiCompilationSecondUnitTestCheck", + ).contains(name) } ) From 1fc313d129928dd8578796cf561f81366c0402ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 20 Sep 2020 10:46:18 +0200 Subject: [PATCH 09/11] Simplify the identification of param specs and captures. We don't use `paramSymss` anymore. We now only work with `paramNamess` and `paramInfoss`, which are reliable. We also clean up a bunch of things by making things "more general", requiring less explanation comments. In particular, we make no difference between the outer param and other capture params. --- .../tools/backend/sjs/JSExportsGen.scala | 141 ++++++++---------- .../src/dotty/tools/dotc/core/Types.scala | 7 +- 2 files changed, 68 insertions(+), 80 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala index 9dda34d27008..07e0f0bf9230 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala @@ -518,7 +518,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { // If argument is undefined and there is a default getter, call it val verifiedOrDefault = if (param.hasDefault) { js.If(js.BinaryOp(js.BinaryOp.===, jsArg, js.Undefined()), { - genCallDefaultGetter(exported.sym, i, param.sym.sourcePos, static) { + genCallDefaultGetter(exported.sym, i, static) { prevArgsCount => result.take(prevArgsCount).toList.map(_.ref) } }, { @@ -537,7 +537,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { result.toList } - private def genCallDefaultGetter(sym: Symbol, paramIndex: Int, paramPos: SourcePosition, static: Boolean)( + private def genCallDefaultGetter(sym: Symbol, paramIndex: Int, static: Boolean)( previousArgsValues: Int => List[js.Tree])( implicit pos: SourcePosition): js.Tree = { @@ -562,7 +562,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { report.error( "When overriding a native method with default arguments, " + "the overriding method must explicitly repeat the default arguments.", - paramPos) + sym.srcPos) js.Undefined() } } else { @@ -570,21 +570,9 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { } } - private def targetSymForDefaultGetter(sym: Symbol): Symbol = { - if (sym.isClassConstructor) { - /*/* Get the companion module class. - * For inner classes the sym.owner.companionModule can be broken, - * therefore companionModule is fetched at uncurryPhase. - */ - val companionModule = enteringPhase(currentRun.namerPhase) { - sym.owner.companionModule - } - companionModule.moduleClass*/ - sym.owner.companionModule.moduleClass - } else { - sym.owner - } - } + private def targetSymForDefaultGetter(sym: Symbol): Symbol = + if (sym.isClassConstructor) sym.owner.companionModule.moduleClass + else sym.owner private def defaultGetterDenot(targetSym: Symbol, sym: Symbol, paramIndex: Int): Denotation = targetSym.info.member(DefaultGetterName(sym.name.asTermName, paramIndex)) @@ -607,9 +595,8 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { js.This()(encodeClassType(sym.owner)) } - def boxIfNeeded(call: js.Tree): js.Tree = { + def boxIfNeeded(call: js.Tree): js.Tree = box(call, atPhase(elimErasedValueTypePhase)(sym.info.resultType)) - } if (currentClassSym.isNonNativeJSClass) { assert(sym.owner == currentClassSym.get, sym.fullName) @@ -627,19 +614,18 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { private def genThrowTypeError(msg: String = "No matching overload")(implicit pos: Position): js.Tree = js.Throw(js.JSNew(js.JSGlobalRef("TypeError"), js.StringLiteral(msg) :: Nil)) - private final class ParamSpec(val sym: Symbol, val info: Type, - val isRepeated: Boolean, val hasDefault: Boolean) { + private final class ParamSpec(val info: Type, val isRepeated: Boolean, val hasDefault: Boolean) { override def toString(): String = - s"ParamSpec(${sym.name}, ${info.show}, isRepeated = $isRepeated, hasDefault = $hasDefault)" + i"ParamSpec($info, isRepeated = $isRepeated, hasDefault = $hasDefault)" } private object ParamSpec { - def apply(methodSym: Symbol, sym: Symbol, infoAtElimRepeated: Type, infoAtElimEVT: Type, + def apply(methodSym: Symbol, infoAtElimRepeated: Type, infoAtElimEVT: Type, methodHasDefaultParams: Boolean, paramIndex: Int): ParamSpec = { val isRepeated = infoAtElimRepeated.isRepeatedParam val info = if (isRepeated) infoAtElimRepeated.repeatedToSingle else infoAtElimEVT val hasDefault = methodHasDefaultParams && defaultGetterDenot(methodSym, paramIndex).exists - new ParamSpec(sym, info, isRepeated, hasDefault) + new ParamSpec(info, isRepeated, hasDefault) } } @@ -650,73 +636,70 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { // params: List[ParamSpec] ; captureParams and captureParamsBack: List[js.ParamDef] val (params, captureParamsFront, captureParamsBack) = { - val paramNamessNow = sym.info.paramNamess - val paramInfosNow = sym.info.paramInfoss.flatten - val paramSymsAtElimRepeated = atPhase(elimRepeatedPhase)(sym.paramSymss.flatten.filter(_.isTerm)) - val (paramNamessAtElimRepeated, paramInfosAtElimRepeated, methodHasDefaultParams) = - atPhase(elimRepeatedPhase)((sym.info.paramNamess, sym.info.paramInfoss.flatten, sym.hasDefaultParams)) - val (paramNamessAtElimEVT, paramInfosAtElimEVT) = - atPhase(elimErasedValueTypePhase)((sym.info.paramNamess, sym.info.paramInfoss.flatten)) - - def buildFormalParams(paramSyms: List[Symbol], paramInfosAtElimRepeated: List[Type], - paramInfosAtElimEVT: List[Type]): IndexedSeq[ParamSpec] = { + val (paramNamesAtElimRepeated, paramInfosAtElimRepeated, methodHasDefaultParams) = + atPhase(elimRepeatedPhase)((sym.info.paramNamess.flatten, sym.info.paramInfoss.flatten, sym.hasDefaultParams)) + val (paramNamesAtElimEVT, paramInfosAtElimEVT) = + atPhase(elimErasedValueTypePhase)((sym.info.firstParamNames, sym.info.firstParamTypes)) + val (paramNamesNow, paramInfosNow) = + (sym.info.firstParamNames, sym.info.firstParamTypes) + + val formalParamCount = paramInfosAtElimRepeated.size + + def buildFormalParams(formalParamInfosAtElimEVT: List[Type]): IndexedSeq[ParamSpec] = { (for { - (paramSym, infoAtElimRepeated, infoAtElimEVT, paramIndex) <- - paramSyms.lazyZip(paramInfosAtElimRepeated).lazyZip(paramInfosAtElimEVT).lazyZip(0 until paramSyms.size) + (infoAtElimRepeated, infoAtElimEVT, paramIndex) <- + paramInfosAtElimRepeated.lazyZip(formalParamInfosAtElimEVT).lazyZip(0 until formalParamCount) } yield { - ParamSpec(sym, paramSym, infoAtElimRepeated, infoAtElimEVT, methodHasDefaultParams, paramIndex) + ParamSpec(sym, infoAtElimRepeated, infoAtElimEVT, methodHasDefaultParams, paramIndex) }).toIndexedSeq } + def buildCaptureParams(namesAndInfosNow: List[(TermName, Type)]): List[js.ParamDef] = { + implicit val pos: Position = sym.span + for ((name, info) <- namesAndInfosNow) yield { + js.ParamDef(freshLocalIdent(name.mangledString), NoOriginalName, toIRType(info), + mutable = false, rest = false) + } + } + if (!isConstructorOfNestedJSClass) { - // Easy case: all params are formal params. - assert(paramInfosAtElimRepeated.size == paramInfosAtElimEVT.size, - s"Found ${paramInfosAtElimRepeated.size} params entering elimRepeated but " + - s"${paramInfosAtElimEVT.size} params entering elimErasedValueType for " + - s"non-lifted symbol ${sym.fullName}") - val formalParams = buildFormalParams(paramSymsAtElimRepeated, paramInfosAtElimRepeated, paramInfosAtElimEVT) + // Easy case: all params are formal params + assert(paramInfosAtElimEVT.size == formalParamCount && paramInfosNow.size == formalParamCount, + s"Found $formalParamCount params entering elimRepeated but ${paramInfosAtElimEVT.size} params entering " + + s"elimErasedValueType and ${paramInfosNow.size} params at the back-end for non-lifted symbol ${sym.fullName}") + val formalParams = buildFormalParams(paramInfosAtElimEVT) (formalParams, Nil, Nil) + } else if (formalParamCount == 0) { + // Fast path: all params are capture params + val captureParams = buildCaptureParams(paramNamesNow.zip(paramInfosNow)) + (IndexedSeq.empty, Nil, captureParams) } else { - /* The `arg$outer` param is added by erasure, following "instructions" - * by explicitouter, while the other capture params are added by - * lambdalift (between elimErasedValueType and now). - * - * Since we cannot reliably get Symbols for parameters created by - * intermediate phases, we have to perform some dance with the - * paramNamess and paramInfoss observed at some phases, combined with - * the paramSymss observed at elimRepeated. + /* Slow path: we have to isolate formal params (which were already present at elimRepeated) + * from capture params (which are later, typically by erasure and/or lambdalift). */ - val hasOuterParam = { - paramInfosAtElimEVT.size == paramInfosAtElimRepeated.size + 1 && - paramNamessAtElimEVT.flatten.head == nme.OUTER + def findStartOfFormalParamsIn(paramNames: List[TermName]): Int = { + val start = paramNames.indexOfSlice(paramNamesAtElimRepeated) + assert(start >= 0, s"could not find formal param names $paramNamesAtElimRepeated in $paramNames") + start } - assert( - hasOuterParam || paramInfosAtElimEVT.size == paramInfosAtElimRepeated.size, - s"Found ${paramInfosAtElimRepeated.size} params entering elimRepeated but " + - s"${paramInfosAtElimEVT.size} params entering elimErasedValueType for " + - s"lifted constructor symbol ${sym.fullName}") - - val startOfFormalParams = paramNamessNow.flatten.indexOfSlice(paramNamessAtElimRepeated.flatten) - val formalParamCount = paramInfosAtElimRepeated.size - - val nonOuterParamInfosAtElimEVT = - if (hasOuterParam) paramInfosAtElimEVT.tail - else paramInfosAtElimEVT - val formalParams = buildFormalParams(paramSymsAtElimRepeated, paramInfosAtElimRepeated, nonOuterParamInfosAtElimEVT) - - val paramNamesAndInfosNow = paramNamessNow.flatten.zip(paramInfosNow) - val (captureParamsFrontNow, restOfParamsNow) = paramNamesAndInfosNow.splitAt(startOfFormalParams) - val captureParamsBackNow = restOfParamsNow.drop(formalParamCount) - def makeCaptureParamDef(nameAndInfo: (TermName, Type)): js.ParamDef = { - implicit val pos: Position = sym.span - js.ParamDef(freshLocalIdent(nameAndInfo._1.mangledString), NoOriginalName, toIRType(nameAndInfo._2), - mutable = false, rest = false) - } + // Find the infos of formal params at elimEVT + val startOfFormalParamsAtElimEVT = findStartOfFormalParamsIn(paramNamesAtElimEVT) + val formalParamInfosAtElimEVT = paramInfosAtElimEVT.drop(startOfFormalParamsAtElimEVT).take(formalParamCount) + + // Build the formal param specs from their infos at elimRepeated and elimEVT + val formalParams = buildFormalParams(formalParamInfosAtElimEVT) + + // Find the formal params now to isolate the capture params (before and after the formal params) + val startOfFormalParamsNow = findStartOfFormalParamsIn(paramNamesNow) + val paramNamesAndInfosNow = paramNamesNow.zip(paramInfosNow) + val (captureParamsFrontNow, restOfParamsNow) = paramNamesAndInfosNow.splitAt(startOfFormalParamsNow) + val captureParamsBackNow = restOfParamsNow.drop(formalParamCount) - val captureParamsFront = captureParamsFrontNow.map(makeCaptureParamDef(_)) - val captureParamsBack = captureParamsBackNow.map(makeCaptureParamDef(_)) + // Build the capture param defs from the isolated capture params + val captureParamsFront = buildCaptureParams(captureParamsFrontNow) + val captureParamsBack = buildCaptureParams(captureParamsBackNow) (formalParams, captureParamsFront, captureParamsBack) } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index a5ee2d46ae6f..19800fc635e6 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1499,13 +1499,18 @@ object Types { case _ => Nil } - /** The parameter types in the first parameter section of a generic type or MethodType, Empty list for others */ final def firstParamTypes(using Context): List[Type] = stripPoly match { case mt: MethodType => mt.paramInfos case _ => Nil } + /** The parameter names in the first parameter section of a generic type or MethodType, Empty list for others */ + final def firstParamNames(using Context): List[TermName] = stripPoly match { + case mt: MethodType => mt.paramNames + case _ => Nil + } + /** Is this either not a method at all, or a parameterless method? */ final def isParameterless(using Context): Boolean = stripPoly match { case mt: MethodType => false From 5bade1ddfe35686211e714a89b46b28f840db228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 23 Sep 2020 13:08:13 +0200 Subject: [PATCH 10/11] 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. --- compiler/src/dotty/tools/dotc/Compiler.scala | 1 + .../tools/dotc/transform/ExplicitOuter.scala | 7 ++ .../transform/sjs/AddLocalJSFakeNews.scala | 99 +++++++++++++++++++ .../transform/sjs/ExplicitJSClasses.scala | 37 ++----- project/Build.scala | 3 + 5 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/sjs/AddLocalJSFakeNews.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 10e779dd9b23..11d72d1d2993 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -99,6 +99,7 @@ class Compiler { new PureStats, // Remove pure stats from blocks new VCElideAllocations, // Peep-hole optimization to eliminate unnecessary value class allocations new ArrayApply, // Optimize `scala.Array.apply([....])` and `scala.Array.apply(..., [....])` into `[...]` + new sjs.AddLocalJSFakeNews, // Adds fake new invocations to local JS classes in calls to `createLocalJSClass` new ElimPolyFunction, // Rewrite PolyFunction subclasses to FunctionN subclasses new TailRec, // Rewrite tail recursion to loops new CompleteJavaEnums, // Fill in constructors for Java enums diff --git a/compiler/src/dotty/tools/dotc/transform/ExplicitOuter.scala b/compiler/src/dotty/tools/dotc/transform/ExplicitOuter.scala index a8e43bc1adaa..f66661ab8b79 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExplicitOuter.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExplicitOuter.scala @@ -384,6 +384,13 @@ object ExplicitOuter { } else Nil + /** If the constructors of the given `cls` need to be passed an outer + * argument, the singleton list with the argument, otherwise Nil. + */ + def argsForNew(cls: ClassSymbol, tpe: Type): List[Tree] = + if (hasOuterParam(cls)) singleton(fixThis(outerPrefix(tpe))) :: Nil + else Nil + /** A path of outer accessors starting from node `start`. `start` defaults to the * context owner's this node. There are two alternative conditions that determine * where the path ends: diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/AddLocalJSFakeNews.scala b/compiler/src/dotty/tools/dotc/transform/sjs/AddLocalJSFakeNews.scala new file mode 100644 index 000000000000..483622c2fb0c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/sjs/AddLocalJSFakeNews.scala @@ -0,0 +1,99 @@ +package dotty.tools +package dotc +package transform +package sjs + +import MegaPhase._ +import core.Constants +import core.Contexts._ +import core.Decorators._ +import core.StdNames.nme +import core.Phases._ +import core.Symbols._ +import core.Types._ +import ast.Trees._ +import dotty.tools.dotc.ast.tpd + +import dotty.tools.backend.sjs.JSDefinitions.jsdefn + +/** Adds fake calls to the constructors of local JS classes in calls to + * `createLocalJSClass`. + * + * Given a call of the form + * {{{ + * scala.scalajs.runtime.createLocalJSClass(classOf[C], jsClassValue, ???) + * }}} + * this phase fills in the `???` with an array of calls to the constructors + * of `C`, like + * {{{ + * [ new C(), new C(???, ???) : Object ] + * }}} + * + * If the class `C` has an outer pointer, as determined by the `ExplicitOuter` + * phase, the calls to the constructor insert a reference to the outer + * instance: + * {{{ + * [ new C(Enclosing.this), new C(Enclosing.this, ???, ???) : Object ] + * }}} + * + * The `LambdaLift` phase will further expand those constructor calls with + * values for captures. The back-end will extract the values of the outer + * pointer and/or the captures to introduce them as JS class captures. + * + * Since we need to insert fake `new C()` calls, this scheme does not work for + * abstract local classes. We therefore reject them as implementation + * restriction in `PrepJSInterop`. + * + * This phase complements `ExplicitJSClasses`. The latter cannot create the + * fake new invocations because that would require inventing sound type + * arguments for the class' type parameters in order not to break Ycheck. + */ +class AddLocalJSFakeNews extends MiniPhase { thisPhase => + import ExplicitOuter.outer + import ast.tpd._ + + override def phaseName: String = AddLocalJSFakeNews.name + + override def isEnabled(using Context): Boolean = + ctx.settings.scalajs.value + + override def runsAfter: Set[String] = Set(Erasure.name) + + override def transformApply(tree: Apply)(using Context): Tree = { + if (tree.symbol == jsdefn.Runtime_createLocalJSClass) { + val classValueArg :: superClassValueArg :: _ :: Nil = tree.args + val cls = classValueArg match { + case Literal(constant) if constant.tag == Constants.ClazzTag => + constant.typeValue.typeSymbol.asClass + case _ => + // this shouldn't happen + report.error(i"unexpected $classValueArg for the first argument to `createLocalJSClass`", classValueArg) + jsdefn.JSObjectClass + } + + val fakeNews = { + val ctors = cls.info.decls.lookupAll(nme.CONSTRUCTOR).toList.reverse + val elems = ctors.map(ctor => fakeNew(cls, ctor.asTerm)) + JavaSeqLiteral(elems, TypeTree(defn.ObjectType)) + } + + cpy.Apply(tree)(tree.fun, classValueArg :: superClassValueArg :: fakeNews :: Nil) + } else { + tree + } + } + + /** Creates a fake invocation of the given class with the given constructor. */ + private def fakeNew(cls: ClassSymbol, ctor: TermSymbol)(using Context): Tree = { + val tycon = cls.typeRef + val outerArgs = outer.argsForNew(cls, tycon) + val nonOuterArgCount = ctor.info.firstParamTypes.size - outerArgs.size + val nonOuterArgs = List.fill(nonOuterArgCount)(ref(defn.Predef_undefined).appliedToNone) + + New(tycon, ctor, outerArgs ::: nonOuterArgs) + } +} + +object AddLocalJSFakeNews { + val name: String = "addLocalJSFakeNews" +} diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index 22aca7f7afda..35e357f8cbda 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -163,13 +163,15 @@ import JSSymUtils._ * val Local\$jsclass: AnyRef = createLocalJSClass( * classOf[Local], * js.constructorOf[ParentJSClass], - * Array[AnyRef](new Local(), ...)) + * ???) * } * }}} * - * Since we need to insert fake `new Inner()`s, this scheme does not work for - * abstract local classes. We therefore reject them as implementation - * restriction in `PrepJSInterop`. + * The third argument `???` is a placeholder, which will be filled in by + * `AddLocalJSFakeNews` with fake new invocations for the all the constructors + * of `Local`. We cannot do it at this phase because that would require + * inventing sound type arguments for the type parameters of `Local` out of + * thin air. * * If the body of `Local` references itself, then the `val Local\$jsclass` is * instead declared as a `var` to work around the cyclic dependency: @@ -526,15 +528,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => val typeRef = tree.tpe val clazzValue = clsOf(typeRef) val superClassCtor = genJSConstructorOf(tree, extractSuperTypeConstructor(tree.rhs)) - val fakeNewInstances = { - /* We need to use `reverse` because the Scope returns elements in reverse order compared to tree definitions. - * The back-end needs the fake News to be in the same order as the corresponding tree definitions. - */ - val ctors = cls.info.decls.lookupAll(nme.CONSTRUCTOR).toList.reverse - val elems = ctors.map(ctor => fakeNew(cls, ctor.asTerm)) - JavaSeqLiteral(elems, TypeTree(defn.AnyRefType)) - } - ref(jsdefn.Runtime_createLocalJSClass).appliedTo(clazzValue, superClassCtor, fakeNewInstances) + ref(jsdefn.Runtime_createLocalJSClass).appliedTo(clazzValue, superClassCtor, ref(defn.Predef_undefined)) } val jsclassVal = state.localClass2jsclassVal(sym) @@ -556,23 +550,6 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => } } - /** Creates a fake invocation of the the given class with the given constructor. */ - def fakeNew(cls: ClassSymbol, ctor: TermSymbol)(using Context): Tree = { - /* TODO This is not entirely good enough, as it break -Ycheck for generic - * classes. Erasure restores the consistency of the fake invocations. - * Improving this is left for later. - */ - - val tycon = cls.typeRef - val targs = cls.typeParams.map(_ => TypeBounds.emptyPolyKind) - val argss = ctor.info.paramInfoss.map(_.map(_ => ref(defn.Predef_undefined))) - - New(tycon) - .select(TermRef(tycon, ctor)) - .appliedToTypes(targs) - .appliedToArgss(argss) - } - // This method, together with transformTypeApply and transformSelect, implements step (E) override def transformApply(tree: Apply)(using Context): Tree = { if (!isFullyApplied(tree)) { diff --git a/project/Build.scala b/project/Build.scala index 1b1955d607f3..5c409be7fb56 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1009,6 +1009,9 @@ object Build { scalaJSLinkerConfig ~= { _.withSemantics(build.TestSuiteLinkerOptions.semantics _) }, scalaJSModuleInitializers in Test ++= build.TestSuiteLinkerOptions.moduleInitializers, + // Perform Ycheck after the Scala.js-specific transformation phases + scalacOptions += "-Ycheck:explicitJSClasses,addLocalJSFakeNews", + jsEnvInput in Test := { val resourceDir = fetchScalaJSSource.value / "test-suite/js/src/test/resources" val f = (resourceDir / "NonNativeJSTypeTestNatives.js").toPath From a91beaebba68297205b5e3716ff72edef1a2c570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 23 Sep 2020 18:20:59 +0200 Subject: [PATCH 11/11] Address a number of review comments. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 70 ++++-- .../tools/backend/sjs/JSExportsGen.scala | 210 +++++++----------- .../transform/sjs/ExplicitJSClasses.scala | 14 +- 3 files changed, 134 insertions(+), 160 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 50c2fec03954..195949ecb985 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -1132,10 +1132,32 @@ class JSCodeGen()(using genCtx: Context) { * This is used for the primary constructor of a non-native JS class, * because those cannot access `this` before the super constructor call. * - * dotc inserts statements before the super constructor call for param - * accessor initializers (including val's and var's declared in the params). - * We move those after the super constructor call, and are therefore - * executed later than for a Scala class. + * Normally, in Scala, param accessors (i.e., fields declared directly in + * constructor parameters) are initialized *before* the super constructor + * call. This is important for cases like + * + * abstract class A { + * def a: Int + * println(a) + * } + * class B(val a: Int) extends A + * + * where `a` is supposed to be correctly initialized by the time `println` + * is executed. + * + * However, in a JavaScript class, this is forbidden: it is not allowed to + * read the `this` value in a constructor before the super constructor call. + * + * Therefore, for JavaScript classes, we specifically move all those early + * assignments after the super constructor call, to comply with JavaScript + * limitations. This clearly introduces a semantic difference in + * initialization order between Scala classes and JavaScript classes, but + * there is nothing we can do about it. That difference in behavior is + * basically spec'ed in Scala.js the language, since specifying it any other + * way would prevent JavaScript classes from ever having constructor + * parameters. + * + * We do the same thing in Scala 2, obviously. */ private def moveAllStatementsAfterSuperConstructorCall(body: js.Tree): js.Tree = { val bodyStats = body match { @@ -1447,7 +1469,7 @@ class JSCodeGen()(using genCtx: Context) { val genBoxedRhs = box(genRhs, atPhase(elimErasedValueTypePhase)(sym.info)) js.Assign(field, genBoxedRhs) } else { - js.Assign(field,genRhs) + js.Assign(field, genRhs) } } @@ -1773,28 +1795,28 @@ class JSCodeGen()(using genCtx: Context) { s"but isInnerNonNativeJSClass = $nestedJSClass") def genArgs: List[js.TreeOrJSSpread] = genActualJSArgs(ctor, args) - - if (cls == jsdefn.JSObjectClass && args.isEmpty) - js.JSObjectConstr(Nil) - else if (cls == jsdefn.JSArrayClass && args.isEmpty) - js.JSArrayConstr(Nil) - else if (cls.isAnonymousClass) - genNewAnonJSClass(cls, jsClassValue.get, args.map(genExpr))(fun.span) - else if (!nestedJSClass) - js.JSNew(genLoadJSConstructor(cls), genArgs) - else if (!atPhase(erasurePhase)(cls.is(ModuleClass))) // LambdaLift removes the ModuleClass flag of lifted classes - js.JSNew(jsClassValue.get, genArgs) - else - genCreateInnerJSModule(cls, jsClassValue.get, args.map(genExpr)) + def genArgsAsClassCaptures: List[js.Tree] = args.map(genExpr) + + jsClassValue.fold { + // Static JS class (by construction, it cannot be a module class, as their News do not reach the back-end) + if (cls == jsdefn.JSObjectClass && args.isEmpty) + js.JSObjectConstr(Nil) + else if (cls == jsdefn.JSArrayClass && args.isEmpty) + js.JSArrayConstr(Nil) + else + js.JSNew(genLoadJSConstructor(cls), genArgs) + } { jsClassVal => + // Nested JS class + if (cls.isAnonymousClass) + genNewAnonJSClass(cls, jsClassVal, genArgsAsClassCaptures)(fun.span) + else if (atPhase(erasurePhase)(cls.is(ModuleClass))) // LambdaLift removes the ModuleClass flag of lifted classes + js.JSNew(js.CreateJSClass(encodeClassName(cls), jsClassVal :: genArgsAsClassCaptures), Nil) + else + js.JSNew(jsClassVal, genArgs) + } } } - /** Gen JS code to create the JS class of an inner JS module class. */ - private def genCreateInnerJSModule(sym: Symbol, jsSuperClassValue: js.Tree, args: List[js.Tree])( - implicit pos: Position): js.Tree = { - js.JSNew(js.CreateJSClass(encodeClassName(sym), jsSuperClassValue :: args), Nil) - } - /** Generate an instance of an anonymous (non-lambda) JS class inline * * @param sym Class to generate the instance of diff --git a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala index 07e0f0bf9230..8142d940ee01 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala @@ -168,126 +168,85 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { alts0 } - val (formalArgs, body) = - if (alts.tail.isEmpty) genExportMethodSingleAlt(alts.head, static) - else genExportMethodMultiAlts(alts, jsName, static) + // Create the formal args registry + val hasVarArg = alts.exists(_.hasRepeatedParam) + val minArgc = alts.map(_.minArgc).min + val maxNonRepeatedArgc = alts.map(_.maxNonRepeatedArgc).max + val needsRestParam = maxNonRepeatedArgc != minArgc || hasVarArg + val formalArgsRegistry = new FormalArgsRegistry(minArgc, needsRestParam) - js.JSMethodDef(flags, genExpr(jsName), formalArgs, body)(OptimizerHints.empty, None) - } + // Generate the list of formal parameters + val formalArgs = formalArgsRegistry.genFormalArgs() - private def genExportMethodSingleAlt(alt: Exported, static: Boolean)( - implicit pos: SourcePosition): (List[js.ParamDef], js.Tree) = { - /* This is a fast path for `genExportMethod` that applies for all methods that - * are not overloaded. In addition to being a fast path, it does a better job - * than `genExportMethodMultiAlts` when the only alternative has default - * parameters, because it avoids a spurious dispatch. + /* Generate the body + * We have a fast-path for methods that are not overloaded. In addition to + * being a fast path, it does a better job than `genExportMethodMultiAlts` + * when the only alternative has default parameters, because it avoids a + * spurious dispatch. * In scalac, the spurious dispatch was avoided by a more elaborate case * generation in `genExportMethod`, which was very convoluted and was not * ported to dotc. */ + val body = + if (alts.tail.isEmpty) genApplyForSingleExported(formalArgsRegistry, alts.head, static) + else genExportMethodMultiAlts(formalArgsRegistry, maxNonRepeatedArgc, alts, jsName, static) - val params = alt.params - val paramsSize = params.size - - val minArgc = { - // Find the first default param or repeated param - val firstOptionalParamIndex = params.indexWhere(p => p.hasDefault || p.isRepeated) - if (firstOptionalParamIndex == -1) paramsSize - else firstOptionalParamIndex - } - - val hasVarArg = alt.hasRepeatedParam - val maxArgc = if (hasVarArg) paramsSize - 1 else paramsSize - val needsRestParam = maxArgc != minArgc || hasVarArg - val formalArgsRegistry = new FormalArgsRegistry(minArgc, needsRestParam) - - val formalArgs = formalArgsRegistry.genFormalArgs() - val body = genApplyForSingleExported(formalArgsRegistry, alt, static) - - (formalArgs, body) + js.JSMethodDef(flags, genExpr(jsName), formalArgs, body)(OptimizerHints.empty, None) } - private def genExportMethodMultiAlts(alts: List[Exported], jsName: JSName, static: Boolean)( - implicit pos: SourcePosition): (List[js.ParamDef], js.Tree) = { - // Factor out methods with variable argument lists. - // They can only be at the end of the lists as enforced by PrepJSExports. - val (varArgMeths, normalMeths) = alts.partition(_.hasRepeatedParam) - - // Highest non-repeated argument count - // For varArgsMeths, we have argc - 1, since a repeated parameter list may also be empty (unlike a normal parameter) - val maxArgc = (varArgMeths.map(_.params.size - 1) ::: normalMeths.map(_.params.size)).max - - // Calculates possible arg counts for normal method - def argCounts(ex: Exported): Seq[Int] = { - val params = ex.params - // Find default param - val dParam = params.indexWhere(_.hasDefault) - if (dParam == -1) Seq(params.size) - else dParam to params.size - } + private def genExportMethodMultiAlts(formalArgsRegistry: FormalArgsRegistry, + maxNonRepeatedArgc: Int, alts: List[Exported], jsName: JSName, static: Boolean)( + implicit pos: SourcePosition): js.Tree = { // Generate tuples (argc, method) - val methodArgCounts = { - // Normal methods - for { - method <- normalMeths - argc <- argCounts(method) - } yield (argc, method) - } ::: { - // Repeated parameter methods - for { - method <- varArgMeths - argc <- method.params.size - 1 to maxArgc - } yield (argc, method) + val methodArgCounts = for { + alt <- alts + argc <- alt.minArgc to (if (alt.hasRepeatedParam) maxNonRepeatedArgc else alt.maxNonRepeatedArgc) + } yield { + (argc, alt) } - // Create the formal args registry - val minArgc = methodArgCounts.minBy(_._1)._1 - val hasVarArg = varArgMeths.nonEmpty - val needsRestParam = maxArgc != minArgc || hasVarArg - val formalArgsRegistry = new FormalArgsRegistry(minArgc, needsRestParam) - - // List of formal parameters - val formalArgs = formalArgsRegistry.genFormalArgs() - // Create a list of (argCount -> methods), sorted by argCount (methods may appear multiple times) - val methodByArgCount: List[(Int, List[Exported])] = + val methodsByArgCount: List[(Int, List[Exported])] = methodArgCounts.groupMap(_._1)(_._2).toList.sortBy(_._1) // sort for determinism + val altsWithVarArgs = alts.filter(_.hasRepeatedParam) + // Generate a case block for each (argCount, methods) tuple // TODO? We could optimize this a bit by putting together all the `argCount`s that have the same methods // (Scala.js for scalac does that, but the code is very convoluted and it's not clear that it is worth it). val cases = for { - (argc, methods) <- methodByArgCount - if methods != varArgMeths // exclude default case we're generating anyways for varargs + (argc, methods) <- methodsByArgCount + if methods != altsWithVarArgs // exclude default case we're generating anyways for varargs } yield { // body of case to disambiguates methods with current count val caseBody = genExportSameArgc(jsName, formalArgsRegistry, methods, static, Some(argc)) - List(js.IntLiteral(argc - minArgc)) -> caseBody + List(js.IntLiteral(argc - formalArgsRegistry.minArgc)) -> caseBody } def defaultCase = { - if (!hasVarArg) + if (altsWithVarArgs.isEmpty) genThrowTypeError() else - genExportSameArgc(jsName, formalArgsRegistry, varArgMeths, static, None) + genExportSameArgc(jsName, formalArgsRegistry, altsWithVarArgs, static, None) } val body = { if (cases.isEmpty) { defaultCase - } else if (cases.tail.isEmpty && !hasVarArg) { + } else if (cases.tail.isEmpty && altsWithVarArgs.isEmpty) { cases.head._2 } else { - assert(needsRestParam, "Trying to read rest param length but needsRestParam is false") val restArgRef = formalArgsRegistry.genRestArgRef() js.Match( js.AsInstanceOf(js.JSSelect(restArgRef, js.StringLiteral("length")), jstpe.IntType), - cases.toList, defaultCase)(jstpe.AnyType) + cases, + defaultCase)( + jstpe.AnyType) } } - (formalArgs, body) + body } /** Resolves method calls to [[alts]] while assuming they have the same parameter count. @@ -469,24 +428,8 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { implicit val pos = exported.pos - // the (single) type of the repeated parameter if any - val repeatedTpe = exported.params.lastOption.withFilter(_.isRepeated).map(_.info) - - val normalArgc = exported.params.size - (if (repeatedTpe.isDefined) 1 else 0) - - // optional repeated parameter list - val jsVarArgPrep = repeatedTpe map { tpe => - val rhs = genJSArrayToVarArgs(formalArgsRegistry.genVarargRef(normalArgc)) - val ident = freshLocalIdent("prep" + normalArgc) - js.VarDef(ident, NoOriginalName, rhs.tpe, mutable = false, rhs) - } - - // normal arguments - val jsArgRefs = - (0 until normalArgc).toList.map(formalArgsRegistry.genArgRef(_)) - - // Generate JS code to prepare arguments (default getters and unboxes) - val jsArgPrep = genPrepareArgs(jsArgRefs, exported, static) ++ jsVarArgPrep + // Generate JS code to prepare arguments (repeated args, default getters and unboxes) + val jsArgPrep = genPrepareArgs(formalArgsRegistry, exported, static) val jsArgPrepRefs = jsArgPrep.map(_.ref) // Combine prep'ed formal arguments with captures @@ -504,30 +447,34 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { /** Generate the necessary JavaScript code to prepare the arguments of an * exported method (unboxing and default parameter handling) */ - private def genPrepareArgs(jsArgs: List[js.Tree], exported: Exported, static: Boolean)( + private def genPrepareArgs(formalArgsRegistry: FormalArgsRegistry, exported: Exported, static: Boolean)( implicit pos: SourcePosition): List[js.VarDef] = { val result = new mutable.ListBuffer[js.VarDef] - for { - (jsArg, (param, i)) <- jsArgs.zip(exported.params.zipWithIndex) - } yield { - // Unboxed argument (if it is defined) - val unboxedArg = unbox(jsArg, param.info) - - // If argument is undefined and there is a default getter, call it - val verifiedOrDefault = if (param.hasDefault) { - js.If(js.BinaryOp(js.BinaryOp.===, jsArg, js.Undefined()), { - genCallDefaultGetter(exported.sym, i, static) { - prevArgsCount => result.take(prevArgsCount).toList.map(_.ref) - } - }, { - // Otherwise, unbox the argument - unboxedArg - })(unboxedArg.tpe) + for ((param, i) <- exported.params.zipWithIndex) yield { + val verifiedOrDefault = if (param.isRepeated) { + genJSArrayToVarArgs(formalArgsRegistry.genVarargRef(i)) } else { - // Otherwise, it is always the unboxed argument - unboxedArg + val jsArg = formalArgsRegistry.genArgRef(i) + + // Unboxed argument (if it is defined) + val unboxedArg = unbox(jsArg, param.info) + + // If argument is undefined and there is a default getter, call it + if (param.hasDefault) { + js.If(js.BinaryOp(js.BinaryOp.===, jsArg, js.Undefined()), { + genCallDefaultGetter(exported.sym, i, static) { + prevArgsCount => result.take(prevArgsCount).toList.map(_.ref) + } + }, { + // Otherwise, unbox the argument + unboxedArg + })(unboxedArg.tpe) + } else { + // Otherwise, it is always the unboxed argument + unboxedArg + } } result += js.VarDef(freshLocalIdent("prep" + i), NoOriginalName, @@ -585,25 +532,21 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { implicit pos: SourcePosition): js.Tree = { val sym = exported.sym + val currentClass = currentClassSym.get - def receiver = { - if (static) - genLoadModule(sym.owner) - else if (sym.owner == defn.ObjectClass) - js.This()(jstpe.ClassType(ir.Names.ObjectClass)) - else - js.This()(encodeClassType(sym.owner)) - } + def receiver = + if (static) genLoadModule(sym.owner) + else js.This()(encodeClassType(currentClass)) def boxIfNeeded(call: js.Tree): js.Tree = box(call, atPhase(elimErasedValueTypePhase)(sym.info.resultType)) - if (currentClassSym.isNonNativeJSClass) { - assert(sym.owner == currentClassSym.get, sym.fullName) + if (currentClass.isNonNativeJSClass) { + assert(sym.owner == currentClass, sym.fullName) boxIfNeeded(genApplyJSClassMethod(receiver, sym, args)) } else { if (sym.isClassConstructor) - js.New(encodeClassName(currentClassSym), encodeMethodSym(sym), args) + js.New(encodeClassName(currentClass), encodeMethodSym(sym), args) else if (sym.isPrivate) boxIfNeeded(genApplyMethodStatically(receiver, sym, args)) else @@ -707,6 +650,15 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { val hasRepeatedParam = params.nonEmpty && params.last.isRepeated + val minArgc = { + // Find the first default param or repeated param + val firstOptionalParamIndex = params.indexWhere(p => p.hasDefault || p.isRepeated) + if (firstOptionalParamIndex == -1) params.size + else firstOptionalParamIndex + } + + val maxNonRepeatedArgc = if (hasRepeatedParam) params.size - 1 else params.size + def pos: SourcePosition = sym.sourcePos def exportArgTypeAt(paramIndex: Int): Type = { @@ -721,10 +673,12 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { def typeInfo: String = sym.info.toString } + // !!! Hash codes of RTTypeTest are meaningless because of InstanceOfTypeTest private sealed abstract class RTTypeTest private case class PrimitiveTypeTest(tpe: jstpe.Type, rank: Int) extends RTTypeTest + // !!! This class does not have a meaningful hash code private case class InstanceOfTypeTest(tpe: Type) extends RTTypeTest { override def equals(that: Any): Boolean = { that match { @@ -831,7 +785,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { m.toList } - private class FormalArgsRegistry(minArgc: Int, needsRestParam: Boolean) { + private class FormalArgsRegistry(val minArgc: Int, needsRestParam: Boolean) { private val fixedParamNames: scala.collection.immutable.IndexedSeq[jsNames.LocalName] = (0 until minArgc).toIndexedSeq.map(_ => freshLocalIdent("arg")(NoPosition).name) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala index 35e357f8cbda..d714151c66d5 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/ExplicitJSClasses.scala @@ -510,9 +510,8 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => if (sym.isClass && isLocalJSClass(sym)) { val jsclassValName = LocalJSClassValueName.fresh(sym.name.toTermName) val jsclassVal = newSymbol(ctx.owner, jsclassValName, EmptyFlags, defn.AnyRefType, coord = tree.span) - val state = myState - state.localClass2jsclassVal(sym) = jsclassVal - state.notYetSelfReferencingLocalClasses += sym + myState.localClass2jsclassVal(sym) = jsclassVal + myState.notYetReferencedLocalClasses += sym } ctx } @@ -521,7 +520,6 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => override def transformTypeDef(tree: TypeDef)(using Context): Tree = { val sym = tree.symbol if (sym.isClass && isLocalJSClass(sym)) { - val state = myState val cls = sym.asClass val rhs = { @@ -531,8 +529,8 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => ref(jsdefn.Runtime_createLocalJSClass).appliedTo(clazzValue, superClassCtor, ref(defn.Predef_undefined)) } - val jsclassVal = state.localClass2jsclassVal(sym) - if (state.notYetSelfReferencingLocalClasses.remove(cls)) { + val jsclassVal = myState.localClass2jsclassVal(sym) + if (myState.notYetReferencedLocalClasses.remove(cls)) { Thicket(List(tree, ValDef(jsclassVal, rhs))) } else { /* We are using `jsclassVal` inside the definition of the class. @@ -679,7 +677,7 @@ class ExplicitJSClasses extends MiniPhase with InfoTransformer { thisPhase => // Use the local `val` that stores the JS class value val state = myState val jsclassVal = state.localClass2jsclassVal(cls) - state.notYetSelfReferencingLocalClasses -= cls + state.notYetReferencedLocalClasses -= cls ref(jsclassVal) } else { // Defer translation to `LoadJSConstructor` to the back-end @@ -728,6 +726,6 @@ object ExplicitJSClasses { private final class MyState { val nestedObject2superTypeConstructor = new MutableSymbolMap[Type] val localClass2jsclassVal = new MutableSymbolMap[TermSymbol] - val notYetSelfReferencingLocalClasses = new util.HashSet[Symbol] + val notYetReferencedLocalClasses = new util.HashSet[Symbol] } }