diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 8ffc9637a001..edaaca86ce67 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -1609,10 +1609,10 @@ class JSCodeGen()(using genCtx: Context) { optimizerHints, Unversioned) } else { val namespace = if (isMethodStaticInIR(sym)) { - if (sym.isPrivate) js.MemberNamespace.PrivateStatic + if (sym.is(Private)) js.MemberNamespace.PrivateStatic else js.MemberNamespace.PublicStatic } else { - if (sym.isPrivate) js.MemberNamespace.Private + if (sym.is(Private)) js.MemberNamespace.Private else js.MemberNamespace.Public } val resultIRType = toIRType(patchedResultType(sym)) @@ -3321,7 +3321,29 @@ class JSCodeGen()(using genCtx: Context) { val genReceiver = genExpr(receiver) if (sym == defn.Any_asInstanceOf) { - genAsInstanceOf(genReceiver, to) + receiver match { + /* This is an optimization for `linkTimeIf(cond)(thenp)(elsep).asInstanceOf[T]`. + * If both `thenp` and `elsep` are subtypes of `T`, the `asInstanceOf` + * is redundant and can be removed. The optimizer already routinely performs + * this optimization. However, that comes too late for the module instance + * field alias analysis performed by `IncOptimizer`. In that case, while the + * desugarer removes the `LinkTimeIf`, the extra `AsInstanceOf` prevents + * aliasing the field. Removing the cast ahead of time in the compiler allows + * field aliases to be recognized in the presence of `LinkTimeIf`s. + */ + case Apply(innerFun, List(cond, thenp, elsep)) + if innerFun.symbol == jsdefn.LinkingInfo_linkTimeIf && + thenp.tpe <:< to && elsep.tpe <:< to => + val genReceiver1 = genReceiver match { + case genReceiver: js.LinkTimeIf => + genReceiver + case _ => + throw FatalError(s"Unexpected tree $genReceiver is generated for $innerFun at: ${tree.sourcePos}") + } + js.LinkTimeIf(genReceiver1.cond, genReceiver1.thenp, genReceiver1.elsep)(toIRType(to))(using genReceiver1.pos) + case _ => + genAsInstanceOf(genReceiver, to) + } } else if (sym == defn.Any_isInstanceOf) { genIsInstanceOf(genReceiver, to) } else { @@ -3748,7 +3770,7 @@ class JSCodeGen()(using genCtx: Context) { /** Gen a statically linked call to an instance method. */ def genApplyMethodMaybeStatically(receiver: js.Tree, method: Symbol, arguments: List[js.Tree])(implicit pos: Position): js.Tree = { - if (method.isPrivate || method.isClassConstructor) + if (method.is(Private) || method.isClassConstructor) genApplyMethodStatically(receiver, method, arguments) else genApplyMethod(receiver, method, arguments) @@ -3757,7 +3779,7 @@ class JSCodeGen()(using genCtx: Context) { /** Gen a dynamically linked call to a Scala method. */ def genApplyMethod(receiver: js.Tree, method: Symbol, arguments: List[js.Tree])( implicit pos: Position): js.Tree = { - assert(!method.isPrivate, + assert(!method.is(Private), s"Cannot generate a dynamic call to private method $method at $pos") js.Apply(js.ApplyFlags.empty, receiver, encodeMethodSym(method), arguments)( toIRType(patchedResultType(method))) @@ -3767,7 +3789,7 @@ class JSCodeGen()(using genCtx: Context) { def genApplyMethodStatically(receiver: js.Tree, method: Symbol, arguments: List[js.Tree])( implicit pos: Position): js.Tree = { val flags = js.ApplyFlags.empty - .withPrivate(method.isPrivate && !method.isClassConstructor) + .withPrivate(method.is(Private) && !method.isClassConstructor) .withConstructor(method.isClassConstructor) js.ApplyStatically(flags, receiver, encodeClassName(method.owner), encodeMethodSym(method), arguments)( @@ -3777,7 +3799,7 @@ class JSCodeGen()(using genCtx: 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.withPrivate(method.isPrivate), + js.ApplyStatic(js.ApplyFlags.empty.withPrivate(method.is(Private)), encodeClassName(method.owner), encodeMethodSym(method), arguments)( toIRType(patchedResultType(method))) } @@ -4103,6 +4125,16 @@ class JSCodeGen()(using genCtx: Context) { js.UnaryOp(js.UnaryOp.UnwrapFromThrowable, js.UnaryOp(js.UnaryOp.CheckNotNull, genArgs1)) + case LINKTIME_IF => + // LinkingInfo.linkTimeIf(cond, thenp, elsep) + val cond = genLinkTimeExpr(args(0)) + val thenp = genExpr(args(1)) + val elsep = genExpr(args(2)) + val tpe = + if (isStat) jstpe.VoidType + else toIRType(tree.tpe) + js.LinkTimeIf(cond, thenp, elsep)(tpe) + case UNION_FROM | UNION_FROM_TYPE_CONSTRUCTOR => /* js.|.from and js.|.fromTypeConstructor * We should not have to deal with those. They have a perfectly valid @@ -4128,6 +4160,91 @@ class JSCodeGen()(using genCtx: Context) { } } + private def genLinkTimeExpr(tree: Tree): js.Tree = { + import dotty.tools.backend.ScalaPrimitivesOps.* + + import primitives.* + + implicit val pos = tree.span + + def invalid(): js.Tree = { + report.error( + "Illegal expression in the condition of a linkTimeIf. " + + "Valid expressions are: boolean and int primitives; " + + "references to link-time properties; " + + "primitive operations on booleans; " + + "and comparisons on ints.", + tree.sourcePos) + js.BooleanLiteral(false) + } + + tree match { + case Literal(c) => + import Constants.* + c.tag match { + case BooleanTag => js.BooleanLiteral(c.booleanValue) + case IntTag => js.IntLiteral(c.intValue) + case _ => invalid() + } + + case Apply(fun, args) => + fun.symbol.getAnnotation(jsdefn.LinkTimePropertyAnnot) match { + case Some(annotation) => + val propName = annotation.argumentConstantString(0).get + js.LinkTimeProperty(propName)(toIRType(tree.tpe)) + + case None if isPrimitive(fun.symbol) => + val code = getPrimitive(fun.symbol) + val receiver = (fun: @unchecked) match { + case fun: Select => fun.qualifier + case fun: Ident => desugarIdent(fun).get.qualifier + } + + def genLhs: js.Tree = genLinkTimeExpr(receiver) + def genRhs: js.Tree = genLinkTimeExpr(args.head) + + def unaryOp(op: js.UnaryOp.Code): js.Tree = + js.UnaryOp(op, genLhs) + def binaryOp(op: js.BinaryOp.Code): js.Tree = + js.BinaryOp(op, genLhs, genRhs) + + toIRType(receiver.tpe) match { + case jstpe.BooleanType => + (code: @switch) match { + case ZNOT => unaryOp(js.UnaryOp.Boolean_!) + case EQ => binaryOp(js.BinaryOp.Boolean_==) + case NE | XOR => binaryOp(js.BinaryOp.Boolean_!=) + case OR => binaryOp(js.BinaryOp.Boolean_|) + case AND => binaryOp(js.BinaryOp.Boolean_&) + case ZOR => js.LinkTimeIf(genLhs, js.BooleanLiteral(true), genRhs)(jstpe.BooleanType) + case ZAND => js.LinkTimeIf(genLhs, genRhs, js.BooleanLiteral(false))(jstpe.BooleanType) + case _ => invalid() + } + + case jstpe.IntType => + (code: @switch) match { + case EQ => binaryOp(js.BinaryOp.Int_==) + case NE => binaryOp(js.BinaryOp.Int_!=) + case LT => binaryOp(js.BinaryOp.Int_<) + case LE => binaryOp(js.BinaryOp.Int_<=) + case GT => binaryOp(js.BinaryOp.Int_>) + case GE => binaryOp(js.BinaryOp.Int_>=) + case _ => invalid() + } + + case _ => + invalid() + } + + case None => // if !isPrimitive + invalid() + } + + case _ => + invalid() + } + } + /** Gen the SJSIR for a reflective call. * * Reflective calls are calls to a structural type field or method that @@ -4283,34 +4400,53 @@ class JSCodeGen()(using genCtx: Context) { * This tries to optimize repeated arguments (varargs) by turning them * into js.WrappedArray instead of Scala wrapped arrays. */ - private def genActualArgs(sym: Symbol, args: List[Tree])( - implicit pos: Position): List[js.Tree] = { - args.map(genExpr) - /*val wereRepeated = exitingPhase(currentRun.typerPhase) { - sym.tpe.params.map(p => isScalaRepeatedParamType(p.tpe)) + private def genActualArgs(sym: Symbol, args: List[Tree]): List[js.Tree] = { + // Fast path for methods that do not have any repeated parameter + val isAnyParamRepeated: Boolean = atPhase(elimRepeatedPhase) { + sym.info.paramInfoss.flatten.exists(_.isRepeatedParam) } - if (wereRepeated.size > args.size) { - // Should not happen, but let's not crash + if (!isAnyParamRepeated) { args.map(genExpr) } else { - /* Arguments that are in excess compared to the type signature after - * erasure are lambda-lifted arguments. They cannot be repeated, hence - * the extension to `false`. + /* Erasure and lambdalift both insert capture params in dotc, at + * unpredictable positions. Therefore, we use the *names* of parameters + * as the only reliable link to whether they were initially repeated. + * We also do this in JSSymUtils.jsParamInfos for JS methods. */ - for ((arg, wasRepeated) <- args.zipAll(wereRepeated, EmptyTree, false)) yield { - if (wasRepeated) { - tryGenRepeatedParamAsJSArray(arg, handleNil = false).fold { - genExpr(arg) - } { genArgs => - genNew(WrappedArrayClass, WrappedArray_ctor, - List(js.JSArrayConstr(genArgs))) + val namesOfRepeatedParams: Set[TermName] = atPhase(elimRepeatedPhase) { + sym.info.paramNamess.flatten.zip(sym.info.paramInfoss.flatten).collect { + case (paramName, paramInfo) if paramInfo.isRepeatedParam => paramName + }.toSet + } + + for ((arg, paramName) <- args.zip(sym.info.paramNamess.flatten)) yield { + if (namesOfRepeatedParams.contains(paramName)) { + /* If the argument is a call to the compiler's chosen `wrapArray` + * method with an array literal as argument, we know it actually + * came from expanded varargs. In that case, rewrite to calling our + * custom `scala.scalajs.runtime.to*VarArgs` method. These methods + * choose the best implementation of varargs depending on the + * target platform. + */ + arg match { + case MaybeAsInstanceOf(wrapArray @ WrapArray(MaybeAsInstanceOf(arrayValue: JavaSeqLiteral))) => + implicit val pos: SourcePosition = wrapArray.sourcePos + js.Apply( + js.ApplyFlags.empty, + js.LoadModule(ScalaJSRuntimeModClassName), + js.MethodIdent(WrapArray.wrapArraySymToToVarArgsName(wrapArray.symbol)), + List(genExpr(arrayValue)) + )(jstpe.ClassType(encodeClassName(defn.SeqClass), nullable = true)) + + case _ => + genExpr(arg) } } else { genExpr(arg) } } - }*/ + } } /** Gen actual actual arguments to a JS method call. @@ -4379,29 +4515,7 @@ class JSCodeGen()(using genCtx: Context) { * `js.Array`. */ private def genJSRepeatedParam(arg: Tree): List[js.TreeOrJSSpread] = { - tryGenRepeatedParamAsJSArray(arg, handleNil = true).getOrElse { - /* Fall back to calling runtime.genTraversableOnce2jsArray - * to perform the conversion to js.Array, then wrap in a Spread - * operator. - */ - implicit val pos: SourcePosition = arg.sourcePos - val jsArrayArg = genModuleApplyMethod( - jsdefn.Runtime_toJSVarArgs, - List(genExpr(arg))) - List(js.JSSpread(jsArrayArg)) - } - } - - /** Try and expand an actual argument to a repeated param `(xs: T*)`. - * - * This method recognizes the shapes of tree generated by the desugaring - * of repeated params in Scala, and expands them. - * If `arg` does not have the shape of a generated repeated param, this - * method returns `None`. - */ - private def tryGenRepeatedParamAsJSArray(arg: Tree, - handleNil: Boolean): Option[List[js.Tree]] = { - implicit val pos = arg.span + implicit val pos: SourcePosition = arg.sourcePos // Given a method `def foo(args: T*)` arg match { @@ -4409,17 +4523,20 @@ class JSCodeGen()(using genCtx: Context) { case MaybeAsInstanceOf(WrapArray(MaybeAsInstanceOf(array: JavaSeqLiteral))) => /* Value classes in arrays are already boxed, so no need to use * the type before erasure. - * TODO Is this true in dotty? */ - Some(array.elems.map(e => box(genExpr(e), e.tpe))) + array.elems.map(e => box(genExpr(e), e.tpe)) // foo() - case Ident(_) if handleNil && arg.symbol == defn.NilModule => - Some(Nil) + case Ident(_) if arg.symbol == defn.NilModule => + Nil // foo(argSeq: _*) - cannot be optimized case _ => - None + /* Fall back to calling runtime.toJSVarArgs to perform the conversion + * to js.Array, then wrap in a Spread operator. + */ + val jsArrayArg = genModuleApplyMethod(jsdefn.Runtime_toJSVarArgs, List(genExpr(arg))) + List(js.JSSpread(jsArrayArg)) } } @@ -4434,16 +4551,32 @@ class JSCodeGen()(using genCtx: Context) { } private object WrapArray { - lazy val isWrapArray: Set[Symbol] = { - val names0 = defn.ScalaValueClasses().map(sym => nme.wrapXArray(sym.name)) - val names1 = names0 ++ Set(nme.wrapRefArray, nme.genericWrapArray) - val symsInPredef = names1.map(defn.ScalaPredefModule.requiredMethod(_)) - val symsInScalaRunTime = names1.map(defn.ScalaRuntimeModule.requiredMethod(_)) - (symsInPredef ++ symsInScalaRunTime).toSet + lazy val wrapArraySymToToVarArgsName: Map[Symbol, MethodName] = { + val SeqClassRef = jstpe.ClassRef(encodeClassName(defn.SeqClass)) + + val items: Seq[(Name, String, jstpe.TypeRef)] = Seq( + (nme.genericWrapArray, "toGenericVarArgs", jswkn.ObjectRef), + (nme.wrapRefArray, "toRefVarArgs", jstpe.ArrayTypeRef(jswkn.ObjectRef, 1)), + (tpd.wrapArrayMethodName(defn.UnitType), "toUnitVarArgs", jstpe.ArrayTypeRef(jstpe.ClassRef(jswkn.BoxedUnitClass), 1)), + (tpd.wrapArrayMethodName(defn.BooleanType), "toBooleanVarArgs", jstpe.ArrayTypeRef(jstpe.BooleanRef, 1)), + (tpd.wrapArrayMethodName(defn.CharType), "toCharVarArgs", jstpe.ArrayTypeRef(jstpe.CharRef, 1)), + (tpd.wrapArrayMethodName(defn.ByteType), "toByteVarArgs", jstpe.ArrayTypeRef(jstpe.ByteRef, 1)), + (tpd.wrapArrayMethodName(defn.ShortType), "toShortVarArgs", jstpe.ArrayTypeRef(jstpe.ShortRef, 1)), + (tpd.wrapArrayMethodName(defn.IntType), "toIntVarArgs", jstpe.ArrayTypeRef(jstpe.IntRef, 1)), + (tpd.wrapArrayMethodName(defn.LongType), "toLongVarArgs", jstpe.ArrayTypeRef(jstpe.LongRef, 1)), + (tpd.wrapArrayMethodName(defn.FloatType), "toFloatVarArgs", jstpe.ArrayTypeRef(jstpe.FloatRef, 1)), + (tpd.wrapArrayMethodName(defn.DoubleType), "toDoubleVarArgs", jstpe.ArrayTypeRef(jstpe.DoubleRef, 1)) + ) + + items.map { case (wrapArrayName, simpleName, argTypeRef) => + val wrapArraySym = defn.getWrapVarargsArrayModule.requiredMethod(wrapArrayName) + val toVarArgsName = MethodName(simpleName, argTypeRef :: Nil, SeqClassRef) + wrapArraySym -> toVarArgsName + }.toMap } def unapply(tree: Apply): Option[Tree] = tree match { - case Apply(wrapArray_?, List(wrapped)) if isWrapArray(wrapArray_?.symbol) => + case Apply(wrapArray_?, List(wrapped)) if wrapArraySymToToVarArgsName.contains(wrapArray_?.symbol) => Some(wrapped) case _ => None @@ -4885,6 +5018,7 @@ object JSCodeGen { private val JLRArrayClassName = ClassName("java.lang.reflect.Array") private val JSObjectClassName = ClassName("scala.scalajs.js.Object") private val JavaScriptExceptionClassName = ClassName("scala.scalajs.js.JavaScriptException") + private val ScalaJSRuntimeModClassName = ClassName("scala.scalajs.runtime.package$") private val ObjectArrayTypeRef = jstpe.ArrayTypeRef(jswkn.ObjectRef, 1) diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index 2f6591763fdd..ebec841125b4 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -85,6 +85,8 @@ final class JSDefinitions()(using Context) { def JSGlobalScopeAnnot(using Context) = JSGlobalScopeAnnotType.symbol.asClass @threadUnsafe lazy val JSNameAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSName") def JSNameAnnot(using Context) = JSNameAnnotType.symbol.asClass + @threadUnsafe lazy val JSOperatorAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSOperator") + def JSOperatorAnnot(using Context) = JSOperatorAnnotType.symbol.asClass @threadUnsafe lazy val JSFullNameAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSFullName") def JSFullNameAnnot(using Context) = JSFullNameAnnotType.symbol.asClass @threadUnsafe lazy val JSBracketAccessAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSBracketAccess") @@ -213,6 +215,14 @@ final class JSDefinitions()(using Context) { @threadUnsafe lazy val WrappedArrayType: TypeRef = requiredClassRef("scala.scalajs.js.WrappedArray") def WrappedArrayClass(using Context) = WrappedArrayType.symbol.asClass + @threadUnsafe lazy val LinkingInfoModuleRef = requiredModuleRef("scala.scalajs.LinkingInfo") + def LinkingInfoModule(using Context) = LinkingInfoModuleRef.symbol + @threadUnsafe lazy val LinkingInfo_linkTimeIfR = LinkingInfoModule.requiredMethodRef("linkTimeIf") + def LinkingInfo_linkTimeIf(using Context) = LinkingInfo_linkTimeIfR.symbol + + @threadUnsafe lazy val LinkTimePropertyAnnotType: TypeRef = requiredClassRef("scala.scalajs.annotation.linkTimeProperty") + def LinkTimePropertyAnnot(using Context) = LinkTimePropertyAnnotType.symbol.asClass + @threadUnsafe lazy val ScalaRunTime_isArrayR = defn.ScalaRuntimeModule.requiredMethodRef("isArray", List(???, ???)) def ScalaRunTime_isArray(using Context): Symbol = ScalaRunTime_isArrayR.symbol diff --git a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala index 41e62094b04f..606916ca4b6e 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala @@ -47,8 +47,10 @@ object JSPrimitives { inline val UNWRAP_FROM_THROWABLE = WRAP_AS_THROWABLE + 1 // js.special.unwrapFromThrowable inline val DEBUGGER = UNWRAP_FROM_THROWABLE + 1 // js.special.debugger - inline val THROW = DEBUGGER + 1 // .throw - inline val NEW_ARRAY = THROW + 1 // scala.runtime.Arrays.newArray + inline val LINKTIME_IF = DEBUGGER + 1 // LinkingInfo.linkTimeIf + + inline val THROW = LINKTIME_IF + 1 // .throw + inline val NEW_ARRAY = THROW + 1 // scala.runtime.Arrays.newArray inline val UNION_FROM = NEW_ARRAY + 1 // js.|.from inline val UNION_FROM_TYPE_CONSTRUCTOR = UNION_FROM + 1 // js.|.fromTypeConstructor @@ -135,6 +137,8 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) { addPrimitive(jsdefn.Special_unwrapFromThrowable, UNWRAP_FROM_THROWABLE) addPrimitive(jsdefn.Special_debugger, DEBUGGER) + addPrimitive(jsdefn.LinkingInfo_linkTimeIf, LINKTIME_IF) + addPrimitive(defn.throwMethod, THROW) addPrimitive(defn.newArrayMethod, NEW_ARRAY) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala index 87ee2be91465..a8ebb05e394e 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala @@ -73,10 +73,16 @@ object JSSymUtils { lazy val pc = sym.info.paramNamess.map(_.size).sum sym.name match { - case nme.apply => Call - case JSUnaryOpMethodName(code) if pc == 0 => UnaryOp(code) - case JSBinaryOpMethodName(code) if pc == 1 => BinaryOp(code) - case _ => default + case nme.apply => + Call + case JSUnaryOpMethodName(code, defaultsToOp) + if (defaultsToOp || sym.hasAnnotation(jsdefn.JSOperatorAnnot)) && pc == 0 => + UnaryOp(code) + case JSBinaryOpMethodName(code, defaultsToOp) + if (defaultsToOp || sym.hasAnnotation(jsdefn.JSOperatorAnnot)) && pc == 1 => + BinaryOp(code) + case _ => + default } } else { default @@ -182,6 +188,7 @@ object JSSymUtils { sym.info.paramNamess.flatten.zip(sym.info.paramInfoss.flatten) val paramInfosAtElimRepeated = atPhase(elimRepeatedPhase) { + // See also JSCodeGen.genActualArgs val list = for ((name, info) <- paramNamesAndTypes) yield { val v = @@ -230,43 +237,70 @@ object JSSymUtils { end sjsNeedsField } + /** Extractor for a `TermName` that *may* be a JS unary operator. + * + * If it may be a JS unary operator, then a method with that name may have + * the `@JSOperator` annotation, and it will be treated as such. + * + * If a method has neither `@JSName` nor `@JSOperator`, then a default is + * chosen. If the `Boolean` value is `true`, the default is to treat the + * method as if it had `@JSOperator`. If it is `false`, the default is *not* + * to treat it as an operator. + * + * Currently, all JS unary operators default to `@JSOperator`. + */ private object JSUnaryOpMethodName { private val map = Map( - nme.UNARY_+ -> js.JSUnaryOp.+, - nme.UNARY_- -> js.JSUnaryOp.-, - nme.UNARY_~ -> js.JSUnaryOp.~, - nme.UNARY_! -> js.JSUnaryOp.! + nme.UNARY_+ -> (js.JSUnaryOp.+, true), + nme.UNARY_- -> (js.JSUnaryOp.-, true), + nme.UNARY_~ -> (js.JSUnaryOp.~, true), + nme.UNARY_! -> (js.JSUnaryOp.!, true), ) - def unapply(name: TermName): Option[js.JSUnaryOp.Code] = + def unapply(name: TermName): Option[(js.JSUnaryOp.Code, Boolean)] = map.get(name) } + /** Extractor for a `TermName` that *may* be a JS binary operator. + * + * If it may be a JS binary operator, then a method with that name may have + * the `@JSOperator` annotation, and it will be treated as such. + * + * If a method has neither `@JSName` nor `@JSOperator`, then a default is + * chosen. If the `Boolean` value is `true`, the default is to treat the + * method as if it had `@JSOperator`. If it is `false`, the default is *not* + * to treat it as an operator. + * + * Most JS binary operators default to `@JSOperator`. Currently, the only + * exception is `**`, for backward compatibility reasons. + */ private object JSBinaryOpMethodName { private val map = Map( - nme.ADD -> js.JSBinaryOp.+, - nme.SUB -> js.JSBinaryOp.-, - nme.MUL -> js.JSBinaryOp.*, - nme.DIV -> js.JSBinaryOp./, - nme.MOD -> js.JSBinaryOp.%, - - nme.LSL -> js.JSBinaryOp.<<, - nme.ASR -> js.JSBinaryOp.>>, - nme.LSR -> js.JSBinaryOp.>>>, - nme.OR -> js.JSBinaryOp.|, - nme.AND -> js.JSBinaryOp.&, - nme.XOR -> js.JSBinaryOp.^, - - nme.LT -> js.JSBinaryOp.<, - nme.LE -> js.JSBinaryOp.<=, - nme.GT -> js.JSBinaryOp.>, - nme.GE -> js.JSBinaryOp.>=, - - nme.ZAND -> js.JSBinaryOp.&&, - nme.ZOR -> js.JSBinaryOp.|| + nme.ADD -> (js.JSBinaryOp.+, true), + nme.SUB -> (js.JSBinaryOp.-, true), + nme.MUL -> (js.JSBinaryOp.*, true), + nme.DIV -> (js.JSBinaryOp./, true), + nme.MOD -> (js.JSBinaryOp.%, true), + + nme.LSL -> (js.JSBinaryOp.<<, true), + nme.ASR -> (js.JSBinaryOp.>>, true), + nme.LSR -> (js.JSBinaryOp.>>>, true), + nme.OR -> (js.JSBinaryOp.|, true), + nme.AND -> (js.JSBinaryOp.&, true), + nme.XOR -> (js.JSBinaryOp.^, true), + + nme.LT -> (js.JSBinaryOp.<, true), + nme.LE -> (js.JSBinaryOp.<=, true), + nme.GT -> (js.JSBinaryOp.>, true), + nme.GE -> (js.JSBinaryOp.>=, true), + + nme.ZAND -> (js.JSBinaryOp.&&, true), + nme.ZOR -> (js.JSBinaryOp.||, true), + + termName("**") -> (js.JSBinaryOp.**, false), ) - def unapply(name: TermName): Option[js.JSBinaryOp.Code] = + def unapply(name: TermName): Option[(js.JSBinaryOp.Code, Boolean)] = map.get(name) } } diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 1bb3c6ae5370..f8051f7af5b0 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -71,7 +71,13 @@ trait ParallelTesting extends RunnerOrchestration: def outDir: JFile def flags: TestFlags def sourceFiles: Array[JFile] - def checkFile: Option[JFile] + def checkFileBasePathCandidates: Array[String] + + final def checkFile: Option[JFile] = + checkFileBasePathCandidates + .iterator + .flatMap(base => Iterator(new JFile(s"$base.$testPlatform.check"), new JFile(s"$base.check"))) + .find(_.exists()) def runClassPath: String = outDir.getPath + JFile.pathSeparator + flags.runClassPath @@ -186,9 +192,8 @@ trait ParallelTesting extends RunnerOrchestration: ) extends TestSource { def sourceFiles: Array[JFile] = files.filter(isSourceFile) - def checkFile: Option[JFile] = - sourceFiles.map(f => new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check"))) - .find(_.exists()) + def checkFileBasePathCandidates: Array[String] = + sourceFiles.map(f => f.getPath.replaceFirst("\\.(scala|java)$", "")) } /** A test source whose files will be compiled separately according to their @@ -221,11 +226,8 @@ trait ParallelTesting extends RunnerOrchestration: def sourceFiles = compilationGroups.map(_._2).flatten.toArray - def checkFile: Option[JFile] = - val platform = - if allToolArgs.getOrElse(ToolName.Target, Nil).nonEmpty then s".$testPlatform" - else "" - Some(new JFile(dir.getPath + platform + ".check")).filter(_.exists) + def checkFileBasePathCandidates: Array[String] = + Array(dir.getPath) } protected def shouldSkipTestSource(testSource: TestSource): Boolean = false diff --git a/project/ScalaLibraryPlugin.scala b/project/ScalaLibraryPlugin.scala index 1d77a2f873b3..e39c55c59d7c 100644 --- a/project/ScalaLibraryPlugin.scala +++ b/project/ScalaLibraryPlugin.scala @@ -6,13 +6,13 @@ import scala.jdk.CollectionConverters.* import java.nio.file.Files import xsbti.VirtualFileRef import sbt.internal.inc.Stamper +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.scalaJSVersion object ScalaLibraryPlugin extends AutoPlugin { override def trigger = noTrigger - private val scala2Version = "2.13.16" - private val scalaJSVersion = "1.19.0" + private val scala2Version = "2.13.16" val fetchScala2ClassFiles = taskKey[(Set[File], File)]("Fetch the files to use that were compiled with Scala 2") val fetchScala2SJSIR = taskKey[(Set[File], File)]("Fetch the .sjsir to use from Scala 2") diff --git a/project/plugins.sbt b/project/plugins.sbt index 510afef8d8aa..241dda64f69b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") diff --git a/tests/run/i17761.scala-js.check b/tests/run/i17761.scala-js.check new file mode 100644 index 000000000000..8be268a93208 --- /dev/null +++ b/tests/run/i17761.scala-js.check @@ -0,0 +1,8 @@ +Normal +test +WrappedVarArgs(class java.lang.String, int) +WrappedVarArgs(test, 42) +Transparent +test +ArraySeq(class java.lang.String, int) +ArraySeq(test, 42) diff --git a/tests/run/i3207.scala-js.check b/tests/run/i3207.scala-js.check new file mode 100644 index 000000000000..587bb7b06def --- /dev/null +++ b/tests/run/i3207.scala-js.check @@ -0,0 +1,2 @@ +WrappedVarArgs() +WrappedVarArgs(A, B) diff --git a/tests/run/i768.check b/tests/run/i768.check new file mode 100644 index 000000000000..82d5befe932b --- /dev/null +++ b/tests/run/i768.check @@ -0,0 +1 @@ +ArraySeq(a, bc) diff --git a/tests/run/i768.scala b/tests/run/i768.scala index 08e2200efca8..9716b9be47c6 100644 --- a/tests/run/i768.scala +++ b/tests/run/i768.scala @@ -4,8 +4,6 @@ case class A(a: String*){ object Test { def main(args: Array[String]) = { - assert(A("a", "bc").s == "ArraySeq(a, bc)") + println(A("a", "bc").s) } } - - diff --git a/tests/run/i768.scala-js.check b/tests/run/i768.scala-js.check new file mode 100644 index 000000000000..ab0d95712ccf --- /dev/null +++ b/tests/run/i768.scala-js.check @@ -0,0 +1 @@ +WrappedVarArgs(a, bc) diff --git a/tests/run/sammy_repeated.scala-js.check b/tests/run/sammy_repeated.scala-js.check new file mode 100644 index 000000000000..5d9c120f0233 --- /dev/null +++ b/tests/run/sammy_repeated.scala-js.check @@ -0,0 +1 @@ +WrappedVarArgs(1) diff --git a/tests/run/tagless.scala-js.check b/tests/run/tagless.scala-js.check new file mode 100644 index 000000000000..d941fde9d79f --- /dev/null +++ b/tests/run/tagless.scala-js.check @@ -0,0 +1,20 @@ +5 +(8 + (-(1 + 2))) +5 +(7 + (-1 * 2)) +35 +7 * (8 + (-(1 + 2))) +tf1Tree = Node(Add,WrappedVarArgs(Node(Lit,WrappedVarArgs(Leaf(8))), Node(Neg,WrappedVarArgs(Node(Add,WrappedVarArgs(Node(Lit,WrappedVarArgs(Leaf(1))), Node(Lit,WrappedVarArgs(Leaf(2))))))))) +tfm1Tree = Node(Add,WrappedVarArgs(Node(Lit,WrappedVarArgs(Leaf(7))), Node(Neg,WrappedVarArgs(Node(Mult,WrappedVarArgs(Node(Lit,WrappedVarArgs(Leaf(1))), Node(Lit,WrappedVarArgs(Leaf(2))))))))) +2 +Not a number: "X" +5 +(8 + (-(1 + 2))) +Node(Add,WrappedVarArgs(Node(Lit,WrappedVarArgs(Leaf(8))), Node(Neg,WrappedVarArgs(Node(Add,WrappedVarArgs(Node(Lit,WrappedVarArgs(Leaf(1))), Node(Lit,WrappedVarArgs(Leaf(2))))))))) +5 +(8 + (-(1 + 2))) +(8 + ((-1) + (-2))) +(8 + ((-1) + (-2))) +(8 + ((-1) + (-2))) +(7 + 1 * (-2)) +7 * (8 + ((-1) + (-2))) diff --git a/tests/run/targetName.scala-js.check b/tests/run/targetName.scala-js.check new file mode 100644 index 000000000000..d8e522610d66 --- /dev/null +++ b/tests/run/targetName.scala-js.check @@ -0,0 +1,3 @@ +strings: WrappedVarArgs(hello) +ints: WrappedVarArgs(1, 2) +objs: WrappedVarArgs(1, hi)