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/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 3ade4820cff8..7dccc47d304a 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)) @@ -443,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 = @@ -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)) @@ -676,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) @@ -767,13 +774,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("") @@ -821,10 +862,13 @@ 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)*/ + case t: Try => + genTry(t, isStat) case app: Apply => genApply(app, isStat) @@ -836,16 +880,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 @@ -908,8 +950,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) => @@ -939,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) @@ -969,14 +1012,16 @@ 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) - /*case EmptyTree => - js.Skip()*/ + case EmptyTree => + js.Skip() case _ => throw new FatalError("Unexpected tree in genExpr: " + @@ -1014,6 +1059,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 @@ -1105,7 +1258,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 +1294,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 @@ -1184,9 +1337,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") } @@ -1245,7 +1398,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 +1608,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 +1640,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 +1728,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 +1809,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 { @@ -1695,9 +1848,11 @@ 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, 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 +1872,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 +1898,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 +1930,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) } } @@ -1879,7 +2033,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 @@ -1898,7 +2052,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}") @@ -1967,9 +2121,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 @@ -1998,9 +2155,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) } @@ -2095,8 +2256,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) { @@ -2105,7 +2266,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( @@ -2149,8 +2310,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))) } @@ -2162,7 +2323,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) } @@ -2199,6 +2360,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. * @@ -2319,7 +2645,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))) @@ -2386,7 +2712,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 @@ -2402,24 +2728,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. + * + * 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. * - * Can be given either the module symbol, or 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) } } @@ -2431,23 +2774,138 @@ 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) } } - /** 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")) + /** 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 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() + } + } + } + + 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)) @@ -2474,6 +2932,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] @@ -2494,6 +2955,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/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 = { 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 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) { 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) + } +}