From 824b1fafffcd4bde67f3e3c287b371d9430cbe0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Apr 2019 11:29:59 +0200 Subject: [PATCH 01/11] Scala.js: Fully mangle names in the backend. In dotty, `mangledString` does not mangle operators that are not at the end of an identifier, nor characters that are not JavaIdentifierPart's. This is OK for the JVM, but not the Scala.js IR. This commit forces full encoding of all characters. --- .../dotty/tools/backend/sjs/JSEncoding.scala | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala index 0bb21529c4ac..d761543fa1cf 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala @@ -206,13 +206,48 @@ object JSEncoding { */ ir.Definitions.BoxedUnitClass } else { - ir.Definitions.encodeClassName(sym.fullName.toString) + ir.Definitions.encodeClassName(fullyMangledString(sym.fullName)) } } private def encodeMemberNameInternal(sym: Symbol)( implicit ctx: Context): String = { - sym.name.toString.replace("_", "$und").replace("~", "$tilde") + fullyMangledString(sym.name) + } + + /** Work around https://github.com/lampepfl/dotty/issues/5936 by bridging + * most (all?) of the gap in encoding so that Dotty.js artifacts are + * compatible with the restrictions on valid IR identifier names. + */ + private def fullyMangledString(name: Name): String = { + val base = name.mangledString + val len = base.length + + // slow path + def encodeFurther(): String = { + val result = new java.lang.StringBuilder() + var i = 0 + while (i != len) { + val c = base.charAt(i) + if (c == '_') + result.append("$und") + else if (Character.isJavaIdentifierPart(c) || c == '.') + result.append(c) + else + result.append("$u%04x".format(c.toInt)) + i += 1 + } + result.toString() + } + + var i = 0 + while (i != len) { + val c = base.charAt(i) + if (c == '_' || !Character.isJavaIdentifierPart(c)) + return encodeFurther() + i += 1 + } + base } def toIRType(tp: Type)(implicit ctx: Context): jstpe.Type = { From 397d715224f0aa0a6f1b182fe00bea3fb88ca127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 18 Apr 2019 13:35:00 +0200 Subject: [PATCH 02/11] Scala.js: Implement JS-specific primitives. Except those related to non-native JS classes. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 171 +++++++++++++++++- .../tools/backend/sjs/JSPrimitives.scala | 3 + 2 files changed, 171 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 3ade4820cff8..f1780dcbe496 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -1184,9 +1184,9 @@ class JSCodeGen()(implicit ctx: Context) { genCoercion(tree, receiver, code) else if (code == JSPrimitives.THROW) genThrow(tree, args) - else /*if (primitives.isJSPrimitive(code)) - genJSPrimitive(tree, receiver, args, code) - else*/ + else if (JSPrimitives.isJSPrimitive(code)) + genJSPrimitive(tree, args, code, isStat) + else throw new FatalError(s"Unknown primitive: ${tree.symbol.fullName} at: $pos") } @@ -2199,6 +2199,171 @@ class JSCodeGen()(implicit ctx: Context) { } } + /** Gen JS code for a Scala.js-specific primitive method */ + private def genJSPrimitive(tree: Apply, args: List[Tree], code: Int, + isStat: Boolean): js.Tree = { + + import JSPrimitives._ + + implicit val pos = tree.span + + def genArgs1: js.Tree = { + assert(args.size == 1, + s"Expected exactly 1 argument for JS primitive $code but got " + + s"${args.size} at $pos") + genExpr(args.head) + } + + def genArgs2: (js.Tree, js.Tree) = { + assert(args.size == 2, + s"Expected exactly 2 arguments for JS primitive $code but got " + + s"${args.size} at $pos") + (genExpr(args.head), genExpr(args.tail.head)) + } + + def genArgsVarLength: List[js.TreeOrJSSpread] = + genActualJSArgs(tree.symbol, args) + + def resolveReifiedJSClassSym(arg: Tree): Symbol = { + def fail(): Symbol = { + ctx.error( + tree.symbol.name.toString + " must be called with a constant " + + "classOf[T] representing a class extending js.Any " + + "(not a trait nor an object)", + tree.sourcePos) + NoSymbol + } + arg match { + case Literal(value) if value.tag == Constants.ClazzTag => + val classSym = value.typeValue.typeSymbol + if (isJSType(classSym) && !classSym.is(Trait) && !classSym.is(ModuleClass)) + classSym + else + fail() + case _ => + fail() + } + } + + (code: @switch) match { + case DYNNEW => + // js.Dynamic.newInstance(clazz)(actualArgs: _*) + val (jsClass, actualArgs) = extractFirstArg(genArgsVarLength) + js.JSNew(jsClass, actualArgs) + + case ARR_CREATE => + // js.Array(elements: _*) + js.JSArrayConstr(genArgsVarLength) + + case CONSTRUCTOROF => + // runtime.constructorOf(clazz) + val classSym = resolveReifiedJSClassSym(args.head) + if (classSym == NoSymbol) + js.Undefined() // compile error emitted by resolveReifiedJSClassSym + else + genLoadJSConstructor(classSym) + + /* + case CREATE_INNER_JS_CLASS | CREATE_LOCAL_JS_CLASS => + // runtime.createInnerJSClass(clazz, superClass) + // runtime.createLocalJSClass(clazz, superClass, fakeNewInstances) + val classSym = resolveReifiedJSClassSym(args(0)) + val superClassValue = genExpr(args(1)) + if (classSym == NoSymbol) { + js.Undefined() // compile error emitted by resolveReifiedJSClassSym + } else { + val captureValues = { + if (code == CREATE_INNER_JS_CLASS) { + val outer = genThis() + List.fill(classSym.info.decls.count(_.isClassConstructor))(outer) + } else { + val ArrayValue(_, fakeNewInstances) = args(2) + fakeNewInstances.flatMap(genCaptureValuesFromFakeNewInstance(_)) + } + } + js.CreateJSClass(encodeClassRef(classSym), + superClassValue :: captureValues) + } + + case WITH_CONTEXTUAL_JS_CLASS_VALUE => + // withContextualJSClassValue(jsclass, inner) + val jsClassValue = genExpr(args(0)) + withScopedVars( + contextualJSClassValue := Some(jsClassValue) + ) { + genStatOrExpr(args(1), isStat) + } + */ + + case LINKING_INFO => + // runtime.linkingInfo + js.JSLinkingInfo() + + case DEBUGGER => + // js.special.debugger() + js.Debugger() + + case UNITVAL => + // BoxedUnit.UNIT, which is the boxed version of () + js.Undefined() + + case JS_NATIVE => + // js.native + ctx.error( + "js.native may only be used as stub implementation in facade types", + tree.sourcePos) + js.Undefined() + + case TYPEOF => + // js.typeOf(arg) + val arg = genArgs1 + genAsInstanceOf(js.JSUnaryOp(js.JSUnaryOp.typeof, arg), defn.StringType) + + case IN => + // js.special.in(arg1, arg2) + val (arg1, arg2) = genArgs2 + js.Unbox(js.JSBinaryOp(js.JSBinaryOp.in, arg1, arg2), 'Z') + + case INSTANCEOF => + // js.special.instanceof(arg1, arg2) + val (arg1, arg2) = genArgs2 + js.Unbox(js.JSBinaryOp(js.JSBinaryOp.instanceof, arg1, arg2), 'Z') + + case DELETE => + // js.special.delete(arg1, arg2) + val (arg1, arg2) = genArgs2 + js.JSDelete(js.JSBracketSelect(arg1, arg2)) + + case FORIN => + /* js.special.forin(arg1, arg2) + * + * We must generate: + * + * val obj = arg1 + * val f = arg2 + * for (val key in obj) { + * f(key) + * } + * + * with temporary vals, because `arg2` must be evaluated only + * once, and after `arg1`. + */ + val (arg1, arg2) = genArgs2 + val objVarDef = js.VarDef(freshLocalIdent("obj"), jstpe.AnyType, + mutable = false, arg1) + val fVarDef = js.VarDef(freshLocalIdent("f"), jstpe.AnyType, + mutable = false, arg2) + val keyVarIdent = freshLocalIdent("key") + val keyVarRef = js.VarRef(keyVarIdent)(jstpe.AnyType) + js.Block( + objVarDef, + fVarDef, + js.ForIn(objVarDef.ref, keyVarIdent, { + js.JSFunctionApply(fVarDef.ref, List(keyVarRef)) + })) + } + } + /** Gen actual actual arguments to Scala method call. * Returns a list of the transformed arguments. * diff --git a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala index 431efc3776e0..a5091517b598 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala @@ -41,6 +41,9 @@ object JSPrimitives { final val LastJSPrimitiveCode = THROW + def isJSPrimitive(code: Int): Boolean = + code >= FirstJSPrimitiveCode && code <= LastJSPrimitiveCode + } class JSPrimitives(ctx: Context) extends DottyPrimitives(ctx) { From de74e42239db5c70fe29cf1ede3d557bb1ace0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 18 Apr 2019 17:18:20 +0200 Subject: [PATCH 03/11] Scala.js: Implement access to the JS global scope. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 275 ++++++++++++++---- .../tools/backend/sjs/JSDefinitions.scala | 4 + .../dotty/tools/backend/sjs/JSPositions.scala | 34 ++- 3 files changed, 249 insertions(+), 64 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index f1780dcbe496..f875de2eb0c3 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -25,6 +25,8 @@ import Phases._ import StdNames._ import dotty.tools.dotc.transform.Erasure +import dotty.tools.dotc.util.SourcePosition +import dotty.tools.dotc.util.Spans.Span import org.scalajs.ir import org.scalajs.ir.{ClassKind, Position, Trees => js, Types => jstpe} @@ -58,7 +60,7 @@ class JSCodeGen()(implicit ctx: Context) { private val primitives = new JSPrimitives(ctx) private val positionConversions = new JSPositions()(ctx) - import positionConversions.{pos2irPos, implicitPos2irPos} + import positionConversions._ // Some state -------------------------------------------------------------- @@ -207,7 +209,7 @@ class JSCodeGen()(implicit ctx: Context) { */ private def genScalaClass(td: TypeDef): js.ClassDef = { val sym = td.symbol.asClass - implicit val pos: Position = sym.span + implicit val pos: SourcePosition = sym.sourcePos assert(!sym.is(Trait), "genScalaClass() must be called only for normal classes: "+sym) @@ -366,6 +368,7 @@ class JSCodeGen()(implicit ctx: Context) { else Some(encodeClassFullNameIdent(sym.superClass)) val jsNativeLoadSpec = { if (sym.is(Trait)) None + else if (sym.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) None else { val path = fullJSNameOf(sym).split('.').toList Some(js.JSNativeLoadSpec.Global(path.head, path.tail)) @@ -507,7 +510,7 @@ class JSCodeGen()(implicit ctx: Context) { } private def genRegisterReflectiveInstantiation(sym: Symbol)( - implicit pos: Position): Option[js.Tree] = { + implicit pos: SourcePosition): Option[js.Tree] = { if (isStaticModule(sym)) genRegisterReflectiveInstantiationForModuleClass(sym) else if (sym.is(ModuleClass)) @@ -519,7 +522,7 @@ class JSCodeGen()(implicit ctx: Context) { } private def genRegisterReflectiveInstantiationForModuleClass(sym: Symbol)( - implicit pos: Position): Option[js.Tree] = { + implicit pos: SourcePosition): Option[js.Tree] = { val fqcnArg = js.StringLiteral(sym.fullName.toString) val runtimeClassArg = js.ClassOf(toTypeRef(sym.info)) val loadModuleFunArg = @@ -534,7 +537,7 @@ class JSCodeGen()(implicit ctx: Context) { } private def genRegisterReflectiveInstantiationForNormalClass(sym: Symbol)( - implicit pos: Position): Option[js.Tree] = { + implicit pos: SourcePosition): Option[js.Tree] = { val ctors = if (sym.is(Abstract)) Nil else sym.info.member(nme.CONSTRUCTOR).alternatives.map(_.symbol).filter(m => !m.is(Private | Protected)) @@ -767,13 +770,47 @@ class JSCodeGen()(implicit ctx: Context) { result } + /** Gen JS code for a tree in expression position (in the IR) or the + * global scope. + */ + def genExprOrGlobalScope(tree: Tree): MaybeGlobalScope = { + implicit def pos: SourcePosition = tree.sourcePos + + tree match { + case _: This => + val sym = tree.symbol + if (sym != currentClassSym.get && sym.is(Module)) + genLoadModuleOrGlobalScope(sym) + else + MaybeGlobalScope.NotGlobalScope(genExpr(tree)) + + case _:Ident | _:Select => + val sym = tree.symbol + if (sym.is(Module)) { + assert(!sym.is(PackageClass), "Cannot use package as value: " + tree) + genLoadModuleOrGlobalScope(sym) + } else { + MaybeGlobalScope.NotGlobalScope(genExpr(tree)) + } + + case Apply(fun, _) => + if (fun.symbol == jsdefn.JSDynamic_global) + MaybeGlobalScope.GlobalScope(pos) + else + MaybeGlobalScope.NotGlobalScope(genExpr(tree)) + + case _ => + MaybeGlobalScope.NotGlobalScope(genExpr(tree)) + } + } + /** Gen JS code for a tree in statement or expression position (in the IR). * * This is the main transformation method. Each node of the Scala AST * is transformed into an equivalent portion of the JS AST. */ private def genStatOrExpr(tree: Tree, isStat: Boolean): js.Tree = { - implicit val pos = tree.span + implicit val pos: SourcePosition = tree.sourcePos ctx.debuglog(" " + tree) ctx.debuglog("") @@ -1105,7 +1142,7 @@ class JSCodeGen()(implicit ctx: Context) { * * regular new */ private def genApplyNew(tree: Apply): js.Tree = { - implicit val pos = tree.span + implicit val pos: SourcePosition = tree.sourcePos val Apply(fun @ Select(New(tpt), nme.CONSTRUCTOR), args) = tree val ctor = fun.symbol @@ -1141,7 +1178,7 @@ class JSCodeGen()(implicit ctx: Context) { * companion object. */ private def genNewHijackedClass(clazz: Symbol, ctor: Symbol, - args: List[js.Tree])(implicit pos: Position): js.Tree = { + args: List[js.Tree])(implicit pos: SourcePosition): js.Tree = { val encodedName = encodeClassFullName(clazz) val moduleClass = clazz.companionModule.moduleClass @@ -1245,7 +1282,7 @@ class JSCodeGen()(implicit ctx: Context) { import dotty.tools.backend.ScalaPrimitivesOps._ import js.UnaryOp._ - implicit val pos = tree.span + implicit val pos: SourcePosition = tree.sourcePos val lhsIRType = toIRType(lhs.tpe) val rhsIRType = toIRType(rhs.tpe) @@ -1455,7 +1492,7 @@ class JSCodeGen()(implicit ctx: Context) { /** Gen JS code for a universal equality test. */ private def genUniversalEqualityOp(ltpe: Type, rtpe: Type, lhs: js.Tree, rhs: js.Tree, code: Int)( - implicit pos: Position): js.Tree = { + implicit pos: SourcePosition): js.Tree = { import dotty.tools.backend.ScalaPrimitivesOps._ @@ -1487,7 +1524,7 @@ class JSCodeGen()(implicit ctx: Context) { /** Gen JS code for a call to Any.== */ private def genEqEqPrimitive(ltpe: Type, rtpe: Type, lsrc: js.Tree, rsrc: js.Tree)( - implicit pos: Position): js.Tree = { + implicit pos: SourcePosition): js.Tree = { ctx.debuglog(s"$ltpe == $rtpe") val lsym = ltpe.widenDealias.typeSymbol.asClass val rsym = rtpe.widenDealias.typeSymbol.asClass @@ -1575,7 +1612,7 @@ class JSCodeGen()(implicit ctx: Context) { /** Gen JS code for a call to Any.## */ private def genScalaHash(tree: Apply, receiver: Tree): js.Tree = { - implicit val pos = tree.span + implicit val pos: SourcePosition = tree.sourcePos genModuleApplyMethod(defn.ScalaRuntimeModule.requiredMethod(nme.hash_), List(genExpr(receiver))) @@ -1656,7 +1693,7 @@ class JSCodeGen()(implicit ctx: Context) { /** Gen a call to the special `throw` method. */ private def genThrow(tree: Apply, args: List[Tree]): js.Tree = { - implicit val pos = tree.span + implicit val pos: SourcePosition = tree.sourcePos val exception = args.head val genException = genExpr(exception) js.Throw { @@ -1697,7 +1734,7 @@ class JSCodeGen()(implicit ctx: Context) { if (isJSType(sym.owner)) { //if (!isScalaJSDefinedJSClass(sym.owner) || isExposed(sym)) - genApplyJSMethodGeneric(tree, sym, genExpr(receiver), genActualJSArgs(sym, args), isStat) + genApplyJSMethodGeneric(tree, sym, genExprOrGlobalScope(receiver), genActualJSArgs(sym, args), isStat) /*else genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args))*/ } else { @@ -1717,11 +1754,11 @@ class JSCodeGen()(implicit ctx: Context) { * - Setters are translated to `Assign` to `JSBracketSelect` */ private def genApplyJSMethodGeneric(tree: Tree, sym: Symbol, - receiver: js.Tree, args: List[js.TreeOrJSSpread], isStat: Boolean, + receiver: MaybeGlobalScope, args: List[js.TreeOrJSSpread], isStat: Boolean, jsSuperClassValue: Option[js.Tree] = None)( implicit pos: Position): js.Tree = { - implicit val pos = tree.span + implicit val pos: SourcePosition = tree.sourcePos def noSpread = !args.exists(_.isInstanceOf[js.JSSpread]) val argc = args.size // meaningful only for methods that don't have varargs @@ -1743,27 +1780,27 @@ class JSCodeGen()(implicit ctx: Context) { val boxedResult = sym.name match { case JSUnaryOpMethodName(code) if argc == 0 => requireNotSuper() - js.JSUnaryOp(code, receiver) + js.JSUnaryOp(code, ruleOutGlobalScope(receiver)) case JSBinaryOpMethodName(code) if argc == 1 => requireNotSuper() - js.JSBinaryOp(code, receiver, requireNotSpread(args.head)) + js.JSBinaryOp(code, ruleOutGlobalScope(receiver), requireNotSpread(args.head)) case nme.apply if !hasExplicitJSEncoding => requireNotSuper() if (jsdefn.isJSThisFunctionClass(sym.owner)) - js.JSBracketMethodApply(receiver, js.StringLiteral("call"), args) + js.JSBracketMethodApply(ruleOutGlobalScope(receiver), js.StringLiteral("call"), args) else - js.JSFunctionApply(receiver, args) + js.JSFunctionApply(ruleOutGlobalScope(receiver), args) case _ => def jsFunName = js.StringLiteral(jsNameOf(sym)) def genSuperReference(propName: js.Tree): js.Tree = { jsSuperClassValue.fold[js.Tree] { - js.JSBracketSelect(receiver, propName) + genJSBracketSelectOrGlobalRef(receiver, propName) } { superClassValue => - js.JSSuperBracketSelect(superClassValue, receiver, propName) + js.JSSuperBracketSelect(superClassValue, ruleOutGlobalScope(receiver), propName) } } @@ -1775,10 +1812,9 @@ class JSCodeGen()(implicit ctx: Context) { def genCall(methodName: js.Tree, args: List[js.TreeOrJSSpread]): js.Tree = { jsSuperClassValue.fold[js.Tree] { - js.JSBracketMethodApply( - receiver, methodName, args) + genJSBracketMethodApplyOrGlobalRefApply(receiver, methodName, args) } { superClassValue => - js.JSSuperBracketCall(superClassValue, receiver, methodName, args) + js.JSSuperBracketCall(superClassValue, ruleOutGlobalScope(receiver), methodName, args) } } @@ -2162,7 +2198,7 @@ class JSCodeGen()(implicit ctx: Context) { /** Gen a call to a method of a Scala top-level module. */ private def genModuleApplyMethod(methodSym: Symbol, arguments: List[js.Tree])( - implicit pos: Position): js.Tree = { + implicit pos: SourcePosition): js.Tree = { genApplyMethod(genLoadModule(methodSym.owner), methodSym, arguments) } @@ -2484,7 +2520,7 @@ class JSCodeGen()(implicit ctx: Context) { * to perform the conversion to js.Array, then wrap in a Spread * operator. */ - implicit val pos = arg.span + implicit val pos: SourcePosition = arg.sourcePos val jsArrayArg = genModuleApplyMethod( jsdefn.Runtime_toJSVarArgs, List(genExpr(arg))) @@ -2551,7 +2587,7 @@ class JSCodeGen()(implicit ctx: Context) { /** Gen JS code for loading a Java static field. */ - private def genLoadStaticField(sym: Symbol)(implicit pos: Position): js.Tree = { + private def genLoadStaticField(sym: Symbol)(implicit pos: SourcePosition): js.Tree = { /* Actually, there is no static member in Scala.js. If we come here, that * is because we found the symbol in a Java-emitted .class in the * classpath. But the corresponding implementation in Scala.js will @@ -2567,24 +2603,41 @@ class JSCodeGen()(implicit ctx: Context) { } } - /** Gen JS code for loading a module. + /** Generate loading of a module value. * - * Can be given either the module symbol, or its module class symbol. + * Can be given either the module symbol or its module class symbol. + * + * If the module we load refers to the global scope (i.e., it is + * annotated with `@JSGlobalScope`), report a compile error specifying + * 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 = + ruleOutGlobalScope(genLoadModuleOrGlobalScope(sym)) + + /** Generate loading of a module value or the global scope. + * + * Can be given either the module symbol of its module class symbol. + * + * Unlike `genLoadModule`, this method does not fail if the module we load + * refers to the global scope. */ - private def genLoadModule(sym0: Symbol)(implicit pos: Position): js.Tree = { + def genLoadModuleOrGlobalScope(sym0: Symbol)( + implicit pos: SourcePosition): MaybeGlobalScope = { + require(sym0.is(Module), "genLoadModule called with non-module symbol: " + sym0) val sym = if (sym0.isTerm) sym0.moduleClass else sym0 - if (isJSType(sym)) { - if (isScalaJSDefinedJSClass(sym)) - js.LoadJSModule(encodeClassRef(sym)) - /*else if (sym.derivesFrom(jsdefn.JSGlobalScopeClass)) - genLoadJSGlobal()*/ - else - genLoadNativeJSModule(sym) + // Does that module refer to the global scope? + if (sym.hasAnnotation(jsdefn.JSGlobalScopeAnnot)) { + MaybeGlobalScope.GlobalScope(pos) } else { - js.LoadModule(encodeClassRef(sym)) + val cls = encodeClassRef(sym) + val tree = + if (isJSType(sym)) js.LoadJSModule(cls) + else js.LoadModule(cls) + MaybeGlobalScope.NotGlobalScope(tree) } } @@ -2596,21 +2649,133 @@ class JSCodeGen()(implicit ctx: Context) { js.LoadJSConstructor(encodeClassRef(sym)) } - /** Gen JS code representing a native JS module. */ - private def genLoadNativeJSModule(sym: Symbol)( - implicit pos: Position): js.Tree = { - require(sym.is(ModuleClass), - s"genLoadNativeJSModule called with non-module $sym") - fullJSNameOf(sym).split('.').foldLeft(genLoadJSGlobal()) { (memo, chunk) => - js.JSBracketSelect(memo, js.StringLiteral(chunk)) + private final val GenericGlobalObjectInformationMsg = { + "\n " + + "See https://www.scala-js.org/doc/interoperability/global-scope.html " + + "for further information." + } + + /** Rule out the `GlobalScope` case of a `MaybeGlobalScope` and extract the + * value tree. + * + * If `tree` represents the global scope, report a compile error. + */ + private def ruleOutGlobalScope(tree: MaybeGlobalScope): js.Tree = { + tree match { + case MaybeGlobalScope.NotGlobalScope(t) => + t + case MaybeGlobalScope.GlobalScope(pos) => + reportErrorLoadGlobalScope()(pos) + } + } + + /** Report a compile error specifying that the global scope cannot be + * loaded as a value. + */ + private def reportErrorLoadGlobalScope()(implicit pos: SourcePosition): js.Tree = { + ctx.error( + "Loading the global scope as a value (anywhere but as the " + + "left-hand-side of a `.`-selection) is not allowed." + + GenericGlobalObjectInformationMsg, + pos) + js.Undefined() + } + + /** Gen a JS bracket select or a `JSGlobalRef`. + * + * If the receiver is a normal value, i.e., not the global scope, then + * emit a `JSBracketSelect`. + * + * Otherwise, if the `item` is a constant string that is a valid + * JavaScript identifier, emit a `JSGlobalRef`. + * + * Otherwise, report a compile error. + */ + private def genJSBracketSelectOrGlobalRef(qual: MaybeGlobalScope, item: js.Tree)( + implicit pos: SourcePosition): js.Tree = { + qual match { + case MaybeGlobalScope.NotGlobalScope(qualTree) => + js.JSBracketSelect(qualTree, item) + + case MaybeGlobalScope.GlobalScope(_) => + item match { + case js.StringLiteral(value) => + if (value == "arguments") { + ctx.error( + "Selecting a field of the global scope whose name is " + + "`arguments` is not allowed." + + GenericGlobalObjectInformationMsg, + pos) + js.JSGlobalRef(js.Ident("erroneous")) + } else if (js.isValidIdentifier(value)) { + js.JSGlobalRef(js.Ident(value)) + } else { + ctx.error( + "Selecting a field of the global scope whose name is " + + "not a valid JavaScript identifier is not allowed." + + GenericGlobalObjectInformationMsg, + pos) + js.JSGlobalRef(js.Ident("erroneous")) + } + + case _ => + ctx.error( + "Selecting a field of the global scope with a dynamic " + + "name is not allowed." + + GenericGlobalObjectInformationMsg, + pos) + js.JSGlobalRef(js.Ident("erroneous")) + } } } - /** Gen JS code to load the JavaScript global scope. */ - private def genLoadJSGlobal()(implicit pos: Position): js.Tree = { - js.JSBracketSelect( - js.JSBracketSelect(js.JSLinkingInfo(), js.StringLiteral("envInfo")), - js.StringLiteral("global")) + /** Gen a JS bracket method apply or an apply of a `GlobalRef`. + * + * If the receiver is a normal value, i.e., not the global scope, then + * emit a `JSBracketMethodApply`. + * + * Otherwise, if the `method` is a constant string that is a valid + * JavaScript identifier, emit a `JSFunctionApply(JSGlobalRef(...), ...)`. + * + * Otherwise, report a compile error. + */ + private def genJSBracketMethodApplyOrGlobalRefApply( + receiver: MaybeGlobalScope, method: js.Tree, args: List[js.TreeOrJSSpread])( + implicit pos: SourcePosition): js.Tree = { + receiver match { + case MaybeGlobalScope.NotGlobalScope(receiverTree) => + js.JSBracketMethodApply(receiverTree, method, args) + + case MaybeGlobalScope.GlobalScope(_) => + method match { + case js.StringLiteral(value) => + if (value == "arguments") { + ctx.error( + "Calling a method of the global scope whose name is " + + "`arguments` is not allowed." + + GenericGlobalObjectInformationMsg, + pos) + js.Undefined() + } else if (js.isValidIdentifier(value)) { + js.JSFunctionApply(js.JSGlobalRef(js.Ident(value)), args) + } else { + ctx.error( + "Calling a method of the global scope whose name is not " + + "a valid JavaScript identifier is not allowed." + + GenericGlobalObjectInformationMsg, + pos) + js.Undefined() + } + + case _ => + ctx.error( + "Calling a method of the global scope with a dynamic " + + "name is not allowed." + + GenericGlobalObjectInformationMsg, + pos) + js.Undefined() + } + } } /** Generate a Class[_] value (e.g. coming from classOf[T]) */ @@ -2659,6 +2824,14 @@ class JSCodeGen()(implicit ctx: Context) { object JSCodeGen { + sealed abstract class MaybeGlobalScope + + object MaybeGlobalScope { + final case class NotGlobalScope(tree: js.Tree) extends MaybeGlobalScope + + final case class GlobalScope(pos: SourcePosition) extends MaybeGlobalScope + } + /** Marker object for undefined parameters in JavaScript semantic calls. * * To be used inside a `js.Transient` node. diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index aff302c74d4f..8dc16ed85231 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -59,6 +59,8 @@ final class JSDefinitions()(implicit ctx: Context) { lazy val JavaScriptExceptionType: TypeRef = ctx.requiredClassRef("scala.scalajs.js.JavaScriptException") def JavaScriptExceptionClass(implicit ctx: Context) = JavaScriptExceptionType.symbol.asClass + lazy val JSGlobalScopeAnnotType: TypeRef = ctx.requiredClassRef("scala.scalajs.js.annotation.JSGlobalScope") + def JSGlobalScopeAnnot(implicit ctx: Context) = JSGlobalScopeAnnotType.symbol.asClass lazy val JSNameAnnotType: TypeRef = ctx.requiredClassRef("scala.scalajs.js.annotation.JSName") def JSNameAnnot(implicit ctx: Context) = JSNameAnnotType.symbol.asClass lazy val JSFullNameAnnotType: TypeRef = ctx.requiredClassRef("scala.scalajs.js.annotation.JSFullName") @@ -89,6 +91,8 @@ final class JSDefinitions()(implicit ctx: Context) { lazy val JSDynamicModuleRef = ctx.requiredModuleRef("scala.scalajs.js.Dynamic") def JSDynamicModule(implicit ctx: Context) = JSDynamicModuleRef.symbol + lazy val JSDynamic_globalR = JSDynamicModule.requiredMethodRef("global") + def JSDynamic_global(implicit ctx: Context) = JSDynamic_globalR.symbol lazy val JSDynamic_newInstanceR = JSDynamicModule.requiredMethodRef("newInstance") def JSDynamic_newInstance(implicit ctx: Context) = JSDynamic_newInstanceR.symbol diff --git a/compiler/src/dotty/tools/backend/sjs/JSPositions.scala b/compiler/src/dotty/tools/backend/sjs/JSPositions.scala index 46c4509229b3..91f727f8d709 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSPositions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSPositions.scala @@ -2,32 +2,40 @@ package dotty.tools.backend.sjs import dotty.tools.dotc.core._ import Contexts._ -import dotty.tools.dotc.util.Spans -import Spans.Span + +import dotty.tools.dotc.util.{SourceFile, SourcePosition} +import dotty.tools.dotc.util.Spans.Span import org.scalajs.ir /** Conversion utilities from dotty Positions to IR Positions. */ class JSPositions()(implicit ctx: Context) { - /** Implicit conversion from dotty Position to ir.Position. */ - implicit def pos2irPos(span: Span): ir.Position = { + private def sourceAndSpan2irPos(source: SourceFile, span: Span): ir.Position = { if (!span.exists) ir.Position.NoPosition else { - val source = pos2irPosCache.toIRSource(ctx.compilationUnit.source) - val sourcePos = ctx.compilationUnit.source.atSpan(span) // dotty positions are 1-based but IR positions are 0-based - ir.Position(source, sourcePos.line-1, sourcePos.column-1) + val irSource = span2irPosCache.toIRSource(source) + val point = span.point + val line = source.offsetToLine(point) - 1 + val column = source.column(point) - 1 + ir.Position(irSource, line, column) } } - /** Implicitly materializes an ir.Position from an implicit dotty Position. */ - implicit def implicitPos2irPos( - implicit span: Span): ir.Position = { - pos2irPos(span) - } + /** Implicit conversion from dotty Span to ir.Position. */ + implicit def span2irPos(span: Span): ir.Position = + sourceAndSpan2irPos(ctx.compilationUnit.source, span) + + /** Implicitly materializes an ir.Position from an implicit dotty Span. */ + implicit def implicitSpan2irPos(implicit span: Span): ir.Position = + span2irPos(span) + + /** Implicitly materializes an ir.Position from an implicit dotty SourcePosition. */ + implicit def implicitSourcePos2irPos(implicit sourcePos: SourcePosition): ir.Position = + sourceAndSpan2irPos(sourcePos.source, sourcePos.span) - private[this] object pos2irPosCache { // scalastyle:ignore + private[this] object span2irPosCache { // scalastyle:ignore import dotty.tools.dotc.util._ private[this] var lastDotcSource: SourceFile = null From ef953e6cb4138677cb1412945e973b233746ea21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Apr 2019 16:25:10 +0200 Subject: [PATCH 04/11] Scala.js: Handle EmptyTree and Try in the back-end. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 129 ++++++++++++++++-- 1 file changed, 120 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index f875de2eb0c3..2f48639b86c5 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -860,8 +860,8 @@ class JSCodeGen()(implicit ctx: Context) { case WhileDo(cond, body) => js.While(genExpr(cond), genStat(body)) - /*case t: Try => - genTry(t, isStat)*/ + case t: Try => + genTry(t, isStat) case app: Apply => genApply(app, isStat) @@ -1012,8 +1012,8 @@ class JSCodeGen()(implicit ctx: Context) { case tree: Closure => genClosure(tree) - /*case EmptyTree => - js.Skip()*/ + case EmptyTree => + js.Skip() case _ => throw new FatalError("Unexpected tree in genExpr: " + @@ -1051,6 +1051,114 @@ class JSCodeGen()(implicit ctx: Context) { } } + /** Gen IR code for a `try..catch` or `try..finally` block. + * + * `try..finally` blocks are compiled straightforwardly to `try..finally` + * blocks of the IR. + * + * `try..catch` blocks are a bit more subtle, as the IR does not have + * type-based selection of exceptions to catch. We thus encode explicitly + * the type tests, like in: + * + * ``` + * try { ... } + * catch (e) { + * if (e.isInstanceOf[IOException]) { ... } + * else if (e.isInstanceOf[Exception]) { ... } + * else { + * throw e; // default, re-throw + * } + * } + * ``` + * + * In addition, there are provisions to handle catching JavaScript + * exceptions (which do not extend `Throwable`) as wrapped in a + * `js.JavaScriptException`. + */ + private def genTry(tree: Try, isStat: Boolean): js.Tree = { + implicit val pos: SourcePosition = tree.sourcePos + val Try(block, catches, finalizer) = tree + + val blockAST = genStatOrExpr(block, isStat) + val resultType = toIRType(tree.tpe) + + val handled = + if (catches.isEmpty) blockAST + else genTryCatch(blockAST, catches, resultType, isStat) + + genStat(finalizer) match { + case js.Skip() => handled + case ast => js.TryFinally(handled, ast) + } + } + + private def genTryCatch(body: js.Tree, catches: List[CaseDef], + resultType: jstpe.Type, + isStat: Boolean)(implicit pos: SourcePosition): js.Tree = { + val exceptIdent = freshLocalIdent("e") + val origExceptVar = js.VarRef(exceptIdent)(jstpe.AnyType) + + val mightCatchJavaScriptException = catches.exists { caseDef => + caseDef.pat match { + case Typed(Ident(nme.WILDCARD), tpt) => + isMaybeJavaScriptException(tpt.tpe) + case Ident(nme.WILDCARD) => + true + case pat @ Bind(_, _) => + isMaybeJavaScriptException(pat.symbol.info) + } + } + + val (exceptValDef, exceptVar) = if (mightCatchJavaScriptException) { + val valDef = js.VarDef(freshLocalIdent("e"), + encodeClassType(defn.ThrowableClass), mutable = false, { + genModuleApplyMethod(jsdefn.Runtime_wrapJavaScriptException, origExceptVar :: Nil) + }) + (valDef, valDef.ref) + } else { + (js.Skip(), origExceptVar) + } + + val elseHandler: js.Tree = js.Throw(origExceptVar) + + val handler = catches.foldRight(elseHandler) { (caseDef, elsep) => + implicit val pos: SourcePosition = caseDef.sourcePos + val CaseDef(pat, _, body) = caseDef + + // Extract exception type and variable + val (tpe, boundVar) = (pat match { + case Typed(Ident(nme.WILDCARD), tpt) => + (tpt.tpe, None) + case Ident(nme.WILDCARD) => + (defn.ThrowableType, None) + case Bind(_, _) => + (pat.symbol.info, Some(encodeLocalSym(pat.symbol))) + }) + + // Generate the body that must be executed if the exception matches + val bodyWithBoundVar = (boundVar match { + case None => + genStatOrExpr(body, isStat) + case Some(bv) => + val castException = genAsInstanceOf(exceptVar, tpe) + js.Block( + js.VarDef(bv, toIRType(tpe), mutable = false, castException), + genStatOrExpr(body, isStat)) + }) + + // Generate the test + if (tpe =:= defn.ThrowableType) { + bodyWithBoundVar + } else { + val cond = genIsInstanceOf(exceptVar, tpe) + js.If(cond, bodyWithBoundVar, elsep)(resultType) + } + } + + js.TryCatch(body, exceptIdent, + js.Block(exceptValDef, handler))(resultType) + } + /** Gen JS code for an Apply node (method call) * * There's a whole bunch of varieties of Apply nodes: regular method @@ -1915,7 +2023,7 @@ class JSCodeGen()(implicit ctx: Context) { * primitive instead.) */ private def genTypeApply(tree: TypeApply): js.Tree = { - implicit val pos = tree.span + implicit val pos: SourcePosition = tree.sourcePos val TypeApply(fun, targs) = tree @@ -1934,7 +2042,7 @@ class JSCodeGen()(implicit ctx: Context) { if (sym == defn.Any_asInstanceOf) { genAsInstanceOf(genReceiver, to) } else if (sym == defn.Any_isInstanceOf) { - genIsInstanceOf(tree, genReceiver, to) + genIsInstanceOf(genReceiver, to) } else { throw new FatalError( s"Unexpected type application $fun with symbol ${sym.fullName}") @@ -2131,8 +2239,8 @@ class JSCodeGen()(implicit ctx: Context) { } /** Gen JS code for an isInstanceOf test (for reference types only) */ - private def genIsInstanceOf(tree: Tree, value: js.Tree, to: Type): js.Tree = { - implicit val pos = tree.span + private def genIsInstanceOf(value: js.Tree, to: Type)( + implicit pos: SourcePosition): js.Tree = { val sym = to.widenDealias.typeSymbol if (sym == defn.ObjectClass) { @@ -2141,7 +2249,7 @@ class JSCodeGen()(implicit ctx: Context) { if (sym.is(Trait)) { ctx.error( s"isInstanceOf[${sym.fullName}] not supported because it is a JS trait", - tree.sourcePos) + pos) js.BooleanLiteral(true) } else { js.Unbox(js.JSBinaryOp( @@ -2804,6 +2912,9 @@ class JSCodeGen()(implicit ctx: Context) { ) } + private def isMaybeJavaScriptException(tpe: Type): Boolean = + jsdefn.JavaScriptExceptionClass.isSubClass(tpe.typeSymbol) + // Copied from DottyBackendInterface private val desugared = new java.util.IdentityHashMap[Type, tpd.Select] From 3b7e081d51ae95966a0814bba67472c15ad54685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Apr 2019 16:44:09 +0200 Subject: [PATCH 05/11] Scala.js: Handle static methods in the backend. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 2f48639b86c5..6e96f7da7dea 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -679,9 +679,13 @@ class JSCodeGen()(implicit ctx: Context) { methodName, jsParams, jstpe.NoType, Some(genStat(rhs)))(optimizerHints, None) } else { - val namespace = + val namespace = if (isMethodStaticInIR(sym)) { + if (sym.isPrivate) js.MemberNamespace.PrivateStatic + else js.MemberNamespace.PublicStatic + } else { if (sym.isPrivate) js.MemberNamespace.Private else js.MemberNamespace.Public + } val resultIRType = toIRType(patchedResultType(sym)) genMethodDef(namespace, methodName, params, resultIRType, rhs, optimizerHints) @@ -873,16 +877,14 @@ class JSCodeGen()(implicit ctx: Context) { genApplyDynamic(app)*/ case tree: This => - if (tree.symbol == currentClassSym.get) { - genThis() - } else { - assert(tree.symbol.is(Module), - "Trying to access the this of another class: " + - "tree.symbol = " + tree.symbol + - ", class symbol = " + currentClassSym.get + - " span:" + pos) + val currentClass = currentClassSym.get + val symIsModuleClass = tree.symbol.is(ModuleClass) + assert(tree.symbol == currentClass || symIsModuleClass, + s"Trying to access the this of another class: tree.symbol = ${tree.symbol}, class symbol = $currentClass") + if (symIsModuleClass && tree.symbol != currentClass) genLoadModule(tree.symbol) - } + else + genThis() case Select(qualifier, _) => val sym = tree.symbol @@ -1840,7 +1842,9 @@ class JSCodeGen()(implicit ctx: Context) { case _ => false } - if (isJSType(sym.owner)) { + if (isMethodStaticInIR(sym)) { + genApplyStatic(sym, genActualArgs(sym, args)) + } else if (isJSType(sym.owner)) { //if (!isScalaJSDefinedJSClass(sym.owner) || isExposed(sym)) genApplyJSMethodGeneric(tree, sym, genExprOrGlobalScope(receiver), genActualJSArgs(sym, args), isStat) /*else @@ -2111,9 +2115,12 @@ class JSCodeGen()(implicit ctx: Context) { case t @ Ident(_) => (t, Nil) } val sym = fun.symbol + val isStaticCall = isMethodStaticInIR(sym) val qualifier = qualifierOf(fun) - val allCaptureValues = qualifier :: env + val allCaptureValues = + if (isStaticCall) env + else qualifier :: env val formalAndActualCaptures = allCaptureValues.map { value => implicit val pos = value.span @@ -2142,9 +2149,13 @@ class JSCodeGen()(implicit ctx: Context) { val (formalParams, actualParams) = formalAndActualParams.unzip val genBody = { - val thisCaptureRef :: argCaptureRefs = formalCaptures.map(_.ref) - val call = genApplyMethodMaybeStatically(thisCaptureRef, sym, - argCaptureRefs ::: actualParams) + val call = if (isStaticCall) { + genApplyStatic(sym, formalCaptures.map(_.ref)) + } else { + val thisCaptureRef :: argCaptureRefs = formalCaptures.map(_.ref) + genApplyMethodMaybeStatically(thisCaptureRef, sym, + argCaptureRefs ::: actualParams) + } box(call, sym.info.finalResultType) } @@ -2293,8 +2304,8 @@ class JSCodeGen()(implicit ctx: Context) { /** Gen a call to a static method. */ private def genApplyStatic(method: Symbol, arguments: List[js.Tree])( implicit pos: Position): js.Tree = { - js.ApplyStatic(js.ApplyFlags.empty, encodeClassRef(method.owner), - encodeMethodSym(method), arguments)( + js.ApplyStatic(js.ApplyFlags.empty.withPrivate(method.isPrivate), + encodeClassRef(method.owner), encodeMethodSym(method), arguments)( toIRType(patchedResultType(method))) } @@ -2886,6 +2897,9 @@ class JSCodeGen()(implicit ctx: Context) { } } + private def isMethodStaticInIR(sym: Symbol): Boolean = + sym.is(JavaStatic, butNot = JavaDefined) + /** Generate a Class[_] value (e.g. coming from classOf[T]) */ private def genClassConstant(tpe: Type)(implicit pos: Position): js.Tree = js.ClassOf(toTypeRef(tpe)) From 83b6e6e35709be37b226e2a853b511ee6c82c384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Apr 2019 18:25:12 +0200 Subject: [PATCH 06/11] Scala.js: Handle Java enum constants in the backend. --- compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 6e96f7da7dea..26b5479f85b9 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -947,8 +947,8 @@ class JSCodeGen()(implicit ctx: Context) { js.Null() case ClazzTag => genClassConstant(value.typeValue) - /*case EnumTag => - genStaticMember(value.symbolValue)*/ + case EnumTag => + genLoadStaticField(value.symbolValue) } case Block(stats, expr) => From baadb0a0fe4f334962b6ada6d552e8a3fd804325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 21 May 2019 14:03:01 +0200 Subject: [PATCH 07/11] Scala.js: Ignore module members when listing fields to emit. --- compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 26b5479f85b9..f41c4f8d8218 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -446,8 +446,8 @@ class JSCodeGen()(implicit ctx: Context) { assert(currentClassSym.get == classSym, "genClassFields called with a ClassDef other than the current one") - // Non-method term members are fields - classSym.info.decls.filter(f => !f.is(Method) && f.isTerm).map({ f => + // Term members that are neither methods nor modules are fields + classSym.info.decls.filter(f => !f.is(Method | Module) && f.isTerm).map({ f => implicit val pos = f.span val name = From 45684e12b24aa25597be7b649ec9fca3063d7de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 21 May 2019 14:03:29 +0200 Subject: [PATCH 08/11] Scala.js: Handle `WhileDo` loops with an `EmptyTree` condition. --- compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index f41c4f8d8218..015c9d1f3a3e 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -862,7 +862,10 @@ class JSCodeGen()(implicit ctx: Context) { }, label) case WhileDo(cond, body) => - js.While(genExpr(cond), genStat(body)) + val genCond = + if (cond == EmptyTree) js.BooleanLiteral(true) + else genExpr(cond) + js.While(genCond, genStat(body)) case t: Try => genTry(t, isStat) From 5683f2f3edaf9dbf9a4512c4ffce65ed4069bf26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 21 May 2019 14:04:54 +0200 Subject: [PATCH 09/11] Scala.js: Do not error when trying to assign to an immutable field. The assertion fails for `OFFSET$x` fields at the moment. We'll re-enable it when that is taken care of. --- compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 015c9d1f3a3e..043d05a6c37d 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -981,8 +981,9 @@ class JSCodeGen()(implicit ctx: Context) { currentMethodSym.get.owner == qualifier.symbol && qualifier.isInstanceOf[This] ) - if (!sym.is(Mutable) && !ctorAssignment) - throw new FatalError(s"Assigning to immutable field ${sym.fullName} at $pos") + // TODO This fails for OFFSET$x fields. Re-enable when we can. + /*if (!sym.is(Mutable) && !ctorAssignment) + throw new FatalError(s"Assigning to immutable field ${sym.fullName} at $pos")*/ val genQual = genExpr(qualifier) From dc619e550fd5f4ec5bf8bbdcceabbf7c35883279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 21 May 2019 14:06:27 +0200 Subject: [PATCH 10/11] Scala.js: Do not crash on `Match` nodes. Instead emit a placeholder `throw null`. Actually producing correct code for `Match` nodes is deferred for now. --- compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 043d05a6c37d..7dccc47d304a 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -1012,8 +1012,10 @@ class JSCodeGen()(implicit ctx: Context) { genJavaSeqLiteral(javaSeqLiteral) /** A Match reaching the backend is supposed to be optimized as a switch */ - /*case mtch: Match => - genMatch(mtch, isStat)*/ + case mtch: Match => + // TODO Correctly handle `Match` nodes + //genMatch(mtch, isStat) + js.Throw(js.Null()) case tree: Closure => genClosure(tree) From 282caf69be949e6c84863cbef42b5231a9e96400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Apr 2019 18:26:05 +0200 Subject: [PATCH 11/11] Add infrastructure to run the JUnit tests of upstream Scala.js. And run one test for now: `compiler/IntTest.scala`. --- .drone.yml | 2 +- build.sbt | 2 + project/Build.scala | 160 ++++++++++++++++++++++---- project/ConstantHolderGenerator.scala | 44 +++++++ 4 files changed, 184 insertions(+), 24 deletions(-) create mode 100644 project/ConstantHolderGenerator.scala diff --git a/.drone.yml b/.drone.yml index c9f7ea243e2c..f94cde83bfa8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -31,7 +31,7 @@ pipeline: image: lampepfl/dotty:2019-04-22 commands: - cp -R . /tmp/2/ && cd /tmp/2/ - - ./project/scripts/sbt ";dotty-bootstrapped/compile ;dotty-bootstrapped/test; dotty-semanticdb/compile; dotty-semanticdb/test:compile;sjsSandbox/run;sjsSandbox/test" + - ./project/scripts/sbt ";dotty-bootstrapped/compile ;dotty-bootstrapped/test; dotty-semanticdb/compile; dotty-semanticdb/test:compile;sjsSandbox/run;sjsSandbox/test;sjsJUnitTests/test" - ./project/scripts/bootstrapCmdTests community_build: diff --git a/build.sbt b/build.sbt index 342a9d834f4c..92703b167726 100644 --- a/build.sbt +++ b/build.sbt @@ -7,6 +7,7 @@ val `dotty-compiler` = Build.`dotty-compiler` val `dotty-compiler-bootstrapped` = Build.`dotty-compiler-bootstrapped` val `dotty-library` = Build.`dotty-library` val `dotty-library-bootstrapped` = Build.`dotty-library-bootstrapped` +val `dotty-library-bootstrappedJS` = Build.`dotty-library-bootstrappedJS` val `dotty-sbt-bridge` = Build.`dotty-sbt-bridge` val `dotty-sbt-bridge-tests` = Build.`dotty-sbt-bridge-tests` val `dotty-language-server` = Build.`dotty-language-server` @@ -23,6 +24,7 @@ val `dist-bootstrapped` = Build.`dist-bootstrapped` val `community-build` = Build.`community-build` val sjsSandbox = Build.sjsSandbox +val sjsJUnitTests = Build.sjsJUnitTests val `sbt-dotty` = Build.`sbt-dotty` val `vscode-dotty` = Build.`vscode-dotty` diff --git a/project/Build.scala b/project/Build.scala index 3808c6b4ca04..688ca5a7b5d8 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -28,6 +28,35 @@ import sbtbuildinfo.BuildInfoPlugin.autoImport._ import scala.util.Properties.isJavaAtLeast +object MyScalaJSPlugin extends AutoPlugin { + import Build._ + + override def requires: Plugins = ScalaJSPlugin + + override def projectSettings: Seq[Setting[_]] = Def.settings( + commonBootstrappedSettings, + + /* Remove the Scala.js compiler plugin for scalac, and enable the + * Scala.js back-end of dotty instead. + */ + libraryDependencies := { + val deps = libraryDependencies.value + deps.filterNot(_.name.startsWith("scalajs-compiler")).map(_.withDottyCompat(scalaVersion.value)) + }, + scalacOptions += "-scalajs", + + // Replace the JVM JUnit dependency by the Scala.js one + libraryDependencies ~= { + _.filter(!_.name.startsWith("junit-interface")) + }, + libraryDependencies += + ("org.scala-js" %% "scalajs-junit-test-runtime" % scalaJSVersion % "test").withDottyCompat(scalaVersion.value), + + // Typecheck the Scala.js IR found on the classpath + scalaJSLinkerConfig ~= (_.withCheckIR(true)), + ) +} + object Build { val scalacVersion = "2.12.8" val referenceVersion = "0.14.0-RC1" @@ -101,6 +130,8 @@ object Build { val ideTestsCompilerArguments = taskKey[Seq[String]]("Compiler arguments to use in IDE tests") val ideTestsDependencyClasspath = taskKey[Seq[File]]("Dependency classpath to use in IDE tests") + val fetchScalaJSSource = taskKey[File]("Fetch the sources of Scala.js") + lazy val SourceDeps = config("sourcedeps") // Settings shared by the build (scoped in ThisBuild). Used in build.sbt @@ -712,6 +743,23 @@ object Build { case Bootstrapped => `dotty-library-bootstrapped` } + /** The dotty standard library compiled with the Scala.js back-end, to produce + * the corresponding .sjsir files. + * + * This artifact must be on the classpath on every "Dotty.js" project. + * + * Currently, only a very small fraction of the dotty library is actually + * included in this project, and hence available to Dotty.js projects. More + * will be added in the future as things are confirmed to be supported. + */ + lazy val `dotty-library-bootstrappedJS`: Project = project.in(file("library-js")). + asDottyLibrary(Bootstrapped). + enablePlugins(MyScalaJSPlugin). + settings( + unmanagedSourceDirectories in Compile := + (unmanagedSourceDirectories in (`dotty-library-bootstrapped`, Compile)).value, + ) + lazy val `dotty-sbt-bridge` = project.in(file("sbt-bridge/src")). // We cannot depend on any bootstrapped project to compile the bridge, since the // bridge is needed to compile these projects. @@ -817,36 +865,102 @@ object Build { * useful, as that would not provide the linker and JS runners. */ lazy val sjsSandbox = project.in(file("sandbox/scalajs")). - enablePlugins(ScalaJSPlugin). - dependsOn(dottyLibrary(Bootstrapped)). - settings(commonBootstrappedSettings). + enablePlugins(MyScalaJSPlugin). + dependsOn(`dotty-library-bootstrappedJS`). settings( - /* Remove the Scala.js compiler plugin for scalac, and enable the - * Scala.js back-end of dotty instead. - */ - libraryDependencies := { - val deps = libraryDependencies.value - deps.filterNot(_.name.startsWith("scalajs-compiler")).map(_.withDottyCompat(scalaVersion.value)) - }, - scalacOptions += "-scalajs", + scalaJSUseMainModuleInitializer := true, + ) + + /** Scala.js test suite. + * + * This project downloads the sources of the upstream Scala.js test suite, + * and tests them with the dotty Scala.js back-end. Currently, only a very + * small fraction of the upstream test suite is actually compiled and run. + * It will grow in the future, as more stuff is confirmed to be supported. + */ + lazy val sjsJUnitTests = project.in(file("tests/sjs-junit")). + enablePlugins(MyScalaJSPlugin). + dependsOn(`dotty-library-bootstrappedJS`). + settings( + scalacOptions --= Seq("-Xfatal-warnings", "-deprecation"), + + sourceDirectory in fetchScalaJSSource := target.value / s"scala-js-src-$scalaJSVersion", + + fetchScalaJSSource := { + import org.eclipse.jgit.api._ + + val s = streams.value + val ver = scalaJSVersion + val trgDir = (sourceDirectory in fetchScalaJSSource).value + + if (!trgDir.exists) { + s.log.info(s"Fetching Scala.js source version $ver") + IO.createDirectory(trgDir) + new CloneCommand() + .setDirectory(trgDir) + .setURI("https://github.com/scala-js/scala-js.git") + .call() + } + + // Checkout proper ref. We do this anyway so we fail if something is wrong + val git = Git.open(trgDir) + s.log.info(s"Checking out Scala.js source version $ver") + git.checkout().setName(s"v$ver").call() - // Replace the JVM JUnit dependency by the Scala.js one - libraryDependencies ~= { - _.filter(!_.name.startsWith("junit-interface")) + trgDir }, + + // We need JUnit in the Compile configuration libraryDependencies += - ("org.scala-js" %% "scalajs-junit-test-runtime" % scalaJSVersion % "test").withDottyCompat(scalaVersion.value), + ("org.scala-js" %% "scalajs-junit-test-runtime" % scalaJSVersion).withDottyCompat(scalaVersion.value), - // The main class cannot be found automatically due to the empty inc.Analysis - mainClass in Compile := Some("hello.HelloWorld"), + sourceGenerators in Compile += Def.task { + import org.scalajs.linker.CheckedBehavior - scalaJSUseMainModuleInitializer := true, + val stage = scalaJSStage.value - /* Debug-friendly Scala.js optimizer options. - * In particular, typecheck the Scala.js IR found on the classpath. - */ - scalaJSLinkerConfig ~= { - _.withCheckIR(true).withParallel(false) + val linkerConfig = stage match { + case FastOptStage => (scalaJSLinkerConfig in (Compile, fastOptJS)).value + case FullOptStage => (scalaJSLinkerConfig in (Compile, fullOptJS)).value + } + + val moduleKind = linkerConfig.moduleKind + val sems = linkerConfig.semantics + + ConstantHolderGenerator.generate( + (sourceManaged in Compile).value, + "org.scalajs.testsuite.utils.BuildInfo", + "scalaVersion" -> scalaVersion.value, + "hasSourceMaps" -> false, //MyScalaJSPlugin.wantSourceMaps.value, + "isNoModule" -> (moduleKind == ModuleKind.NoModule), + "isESModule" -> (moduleKind == ModuleKind.ESModule), + "isCommonJSModule" -> (moduleKind == ModuleKind.CommonJSModule), + "isFullOpt" -> (stage == FullOptStage), + "compliantAsInstanceOfs" -> (sems.asInstanceOfs == CheckedBehavior.Compliant), + "compliantArrayIndexOutOfBounds" -> (sems.arrayIndexOutOfBounds == CheckedBehavior.Compliant), + "compliantModuleInit" -> (sems.moduleInit == CheckedBehavior.Compliant), + "strictFloats" -> sems.strictFloats, + "productionMode" -> sems.productionMode, + "es2015" -> linkerConfig.esFeatures.useECMAScript2015, + ) + }.taskValue, + + managedSources in Compile ++= { + val dir = fetchScalaJSSource.value / "test-suite/js/src/main/scala" + val filter = ( + ("*.scala": FileFilter) + -- "Typechecking*.scala" + -- "NonNativeTypeTestSeparateRun.scala" + ) + (dir ** filter).get + }, + + managedSources in Test ++= { + val dir = fetchScalaJSSource.value / "test-suite" + ( + (dir / "shared/src/test/scala/org/scalajs/testsuite/compiler" ** "IntTest.scala").get + ++ (dir / "shared/src/test/scala/org/scalajs/testsuite/utils" ** "*.scala").get + ) } ) diff --git a/project/ConstantHolderGenerator.scala b/project/ConstantHolderGenerator.scala new file mode 100644 index 000000000000..9dc45313861d --- /dev/null +++ b/project/ConstantHolderGenerator.scala @@ -0,0 +1,44 @@ +import scala.annotation.tailrec + +import sbt._ + +import org.scalajs.ir.ScalaJSVersions + +object ConstantHolderGenerator { + /** Generate a *.scala file that contains the given values as literals. */ + def generate(dir: File, fqn: String, values: (String, Any)*): Seq[File] = { + val (fullPkg@(_ :+ pkg)) :+ objectName = fqn.split('.').toSeq + + val out = dir / (objectName + ".scala") + + val defs = for { + (name, value) <- values + } yield { + s"val $name = ${literal(value)}" + } + + val scalaCode = + s""" + package ${fullPkg.mkString(".")} + + private[$pkg] object $objectName { + ${defs.mkString("\n")} + } + """ + + IO.write(out, scalaCode) + + Seq(out) + } + + @tailrec + private final def literal(v: Any): String = v match { + case s: String => "raw\"\"\"" + s + "\"\"\"" + case b: Boolean => b.toString + case f: File => literal(f.getAbsolutePath) + + case _ => + throw new IllegalArgumentException( + "Unsupported value type: " + v.getClass) + } +}