diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml new file mode 100644 index 0000000000..9b8170126d --- /dev/null +++ b/.github/workflows/windows-ci.yml @@ -0,0 +1,51 @@ +name: Windows CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + SBT_OPTS: '-Xmx6g -Xms1g -Xss4m' + +jobs: + build: + strategy: + matrix: + java: [ '8' ] + + # Use the latest supported version. We will be less affected by ambient changes + # due to the lack of pinning than by breakages because of changing version support. + runs-on: windows-latest + + steps: + - name: Set up git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - uses: actions/checkout@v3 + - name: Set up JDK ${{ matrix.java }} + uses: coursier/setup-action@v1 + with: + jvm: temurin:1.${{ matrix.java }} + apps: sbt + - uses: actions/setup-node@v4 + with: + node-version: '24.x' + cache: 'npm' + - name: npm install + run: npm install + + # Very far from testing everything, but at least it is a good sanity check + - name: Test suite + run: sbt testSuite2_12/test + - name: Linker test suite + run: sbt linker2_12/test + # partest is slow; only execute one test as a smoke test + - name: partest smoke test + run: sbt "partestSuite2_12/testOnly -- --fastOpt run/option-fold.scala" + # Module splitting has some logic for case-insensitive filesystems, which we must test on Windows + - name: Test suite with module splitting + run: sbt 'set testSuite.v2_12/scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule).withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("org.scalajs.testsuite"))))' testSuite2_12/test + shell: bash # for the special characters in the command diff --git a/Jenkinsfile b/Jenkinsfile index 165dec8254..c1a4c70069 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -263,6 +263,11 @@ def Tasks = [ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ + $testSuite$v/test && sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("org.scalajs.testsuite"))))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4f0c93e14c..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: '{build}' -image: Visual Studio 2015 -environment: - global: - NODEJS_VERSION: "16" - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 -install: - - ps: Install-Product node $env:NODEJS_VERSION - - npm install - - cmd: choco install sbt --version 1.3.12 -ia "INSTALLDIR=""C:\sbt""" - - cmd: SET PATH=C:\sbt\bin;%JAVA_HOME%\bin;%PATH% - - cmd: SET "SBT_OPTS=-Xmx4g -Xms4m" -build: off -test_script: - # Very far from testing everything, but at least it is a good sanity check - # For slow things (partest and scripted), we execute only one test - - cmd: sbt ";clean;testSuite2_12/test;linker2_12/test;partestSuite2_12/testOnly -- --fastOpt run/option-fold.scala" - # Module splitting has some logic for case-insensitive filesystems, which we must test on Windows - - cmd: sbt ";setSmallESModulesForAppVeyorCI;testSuite2_12/test" -cache: - - C:\sbt - - C:\Users\appveyor\.ivy2\cache - - C:\Users\appveyor\.sbt diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index e46b1dc14f..3839c61e8c 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2298,50 +2298,17 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) isJSFunctionDef(currentClassSym)) { val flags = js.MemberFlags.empty.withNamespace(namespace) val body = { - def genAsUnaryOp(op: js.UnaryOp.Code): js.Tree = - js.UnaryOp(op, genThis()) - def genAsBinaryOp(op: js.BinaryOp.Code): js.Tree = - js.BinaryOp(op, genThis(), jsParams.head.ref) - def genAsBinaryOpRhsNotNull(op: js.BinaryOp.Code): js.Tree = - js.BinaryOp(op, genThis(), js.UnaryOp(js.UnaryOp.CheckNotNull, jsParams.head.ref)) - - if (currentClassSym.get == HackedStringClass) { - /* Hijack the bodies of String.length and String.charAt and replace - * them with String_length and String_charAt operations, respectively. - */ - methodName.name match { - case `lengthMethodName` => genAsUnaryOp(js.UnaryOp.String_length) - case `charAtMethodName` => genAsBinaryOp(js.BinaryOp.String_charAt) - case _ => genBody() - } - } else if (currentClassSym.get == ClassClass) { - // Similar, for the Class_x operations - methodName.name match { - case `getNameMethodName` => genAsUnaryOp(js.UnaryOp.Class_name) - case `isPrimitiveMethodName` => genAsUnaryOp(js.UnaryOp.Class_isPrimitive) - case `isInterfaceMethodName` => genAsUnaryOp(js.UnaryOp.Class_isInterface) - case `isArrayMethodName` => genAsUnaryOp(js.UnaryOp.Class_isArray) - case `getComponentTypeMethodName` => genAsUnaryOp(js.UnaryOp.Class_componentType) - case `getSuperclassMethodName` => genAsUnaryOp(js.UnaryOp.Class_superClass) - - case `isInstanceMethodName` => genAsBinaryOp(js.BinaryOp.Class_isInstance) - case `isAssignableFromMethodName` => genAsBinaryOpRhsNotNull(js.BinaryOp.Class_isAssignableFrom) - case `castMethodName` => genAsBinaryOp(js.BinaryOp.Class_cast) - - case _ => genBody() - } - } else if (currentClassSym.get == JavaLangReflectArrayModClass) { - methodName.name match { - case `arrayNewInstanceMethodName` => - val List(jlClassParam, lengthParam) = jsParams - js.BinaryOp(js.BinaryOp.Class_newArray, - js.UnaryOp(js.UnaryOp.CheckNotNull, jlClassParam.ref), - lengthParam.ref) - case _ => + val classOwner = currentClassSym.owner + if (classOwner != JavaLangPackageClass && classOwner.owner != JavaLangPackageClass) { + // Fast path; it cannot be any of the special methods of the javalib + genBody() + } else { + JavalibMethodsWithOpBody.get((encodeClassName(currentClassSym), methodName.name)) match { + case None => genBody() + case Some(javalibOpBody) => + javalibOpBody.generate(genThis(), jsParams.map(_.ref)) } - } else { - genBody() } } js.MethodDef(flags, methodName, originalName, jsParams, resultIRType, @@ -4559,50 +4526,78 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (opType == jstpe.AnyType) rsrc_in else adaptPrimitive(rsrc_in, if (isShift) jstpe.IntType else opType) + def regular(op: js.BinaryOp.Code): js.Tree = + js.BinaryOp(op, lsrc, rsrc) + (opType: @unchecked) match { case jstpe.IntType => - val op = (code: @switch) match { - case ADD => Int_+ - case SUB => Int_- - case MUL => Int_* - case DIV => Int_/ - case MOD => Int_% - case OR => Int_| - case AND => Int_& - case XOR => Int_^ - case LSL => Int_<< - case LSR => Int_>>> - case ASR => Int_>> - case EQ => Int_== - case NE => Int_!= - case LT => Int_< - case LE => Int_<= - case GT => Int_> - case GE => Int_>= + def comparison(signedOp: js.BinaryOp.Code, unsignedOp: js.BinaryOp.Code): js.Tree = { + (lsrc, rsrc) match { + case (IntFlipSign(flippedLhs), IntFlipSign(flippedRhs)) => + js.BinaryOp(unsignedOp, flippedLhs, flippedRhs) + case (IntFlipSign(flippedLhs), js.IntLiteral(r)) => + js.BinaryOp(unsignedOp, flippedLhs, js.IntLiteral(r ^ Int.MinValue)(rsrc.pos)) + case (js.IntLiteral(l), IntFlipSign(flippedRhs)) => + js.BinaryOp(unsignedOp, js.IntLiteral(l ^ Int.MinValue)(lsrc.pos), flippedRhs) + case _ => + regular(signedOp) + } + } + + (code: @switch) match { + case ADD => regular(Int_+) + case SUB => regular(Int_-) + case MUL => regular(Int_*) + case DIV => regular(Int_/) + case MOD => regular(Int_%) + case OR => regular(Int_|) + case AND => regular(Int_&) + case XOR => regular(Int_^) + case LSL => regular(Int_<<) + case LSR => regular(Int_>>>) + case ASR => regular(Int_>>) + case EQ => regular(Int_==) + case NE => regular(Int_!=) + + case LT => comparison(Int_<, Int_unsigned_<) + case LE => comparison(Int_<=, Int_unsigned_<=) + case GT => comparison(Int_>, Int_unsigned_>) + case GE => comparison(Int_>=, Int_unsigned_>=) } - js.BinaryOp(op, lsrc, rsrc) case jstpe.LongType => - val op = (code: @switch) match { - case ADD => Long_+ - case SUB => Long_- - case MUL => Long_* - case DIV => Long_/ - case MOD => Long_% - case OR => Long_| - case XOR => Long_^ - case AND => Long_& - case LSL => Long_<< - case LSR => Long_>>> - case ASR => Long_>> - case EQ => Long_== - case NE => Long_!= - case LT => Long_< - case LE => Long_<= - case GT => Long_> - case GE => Long_>= + def comparison(signedOp: js.BinaryOp.Code, unsignedOp: js.BinaryOp.Code): js.Tree = { + (lsrc, rsrc) match { + case (LongFlipSign(flippedLhs), LongFlipSign(flippedRhs)) => + js.BinaryOp(unsignedOp, flippedLhs, flippedRhs) + case (LongFlipSign(flippedLhs), js.LongLiteral(r)) => + js.BinaryOp(unsignedOp, flippedLhs, js.LongLiteral(r ^ Long.MinValue)(rsrc.pos)) + case (js.LongLiteral(l), LongFlipSign(flippedRhs)) => + js.BinaryOp(unsignedOp, js.LongLiteral(l ^ Long.MinValue)(lsrc.pos), flippedRhs) + case _ => + regular(signedOp) + } + } + + (code: @switch) match { + case ADD => regular(Long_+) + case SUB => regular(Long_-) + case MUL => regular(Long_*) + case DIV => regular(Long_/) + case MOD => regular(Long_%) + case OR => regular(Long_|) + case XOR => regular(Long_^) + case AND => regular(Long_&) + case LSL => regular(Long_<<) + case LSR => regular(Long_>>>) + case ASR => regular(Long_>>) + case EQ => regular(Long_==) + case NE => regular(Long_!=) + case LT => comparison(Long_<, Long_unsigned_<) + case LE => comparison(Long_<=, Long_unsigned_<=) + case GT => comparison(Long_>, Long_unsigned_>) + case GE => comparison(Long_>=, Long_unsigned_>=) } - js.BinaryOp(op, lsrc, rsrc) case jstpe.FloatType => def withFloats(op: Int): js.Tree = @@ -5287,11 +5282,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) genStatOrExpr(args(1), isStat) } - case IDENTITY_HASH_CODE => - // runtime.identityHashCode(arg) - val arg = genArgs1 - js.UnaryOp(js.UnaryOp.IdentityHashCode, arg) - case DEBUGGER => // js.special.debugger() js.Debugger() @@ -5511,6 +5501,16 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) 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 LINKTIME_PROPERTY => // LinkingInfo.linkTimePropertyXXX("...") val arg = genArgs1 @@ -5529,6 +5529,83 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } + private def genLinkTimeExpr(tree: Tree): js.Tree = { + import scalaPrimitives._ + + implicit val pos = tree.pos + + def invalid(): js.Tree = { + reporter.error(tree.pos, + "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.") + js.BooleanLiteral(false) + } + + tree match { + case Literal(c) => + c.tag match { + case BooleanTag => js.BooleanLiteral(c.booleanValue) + case IntTag => js.IntLiteral(c.intValue) + case _ => invalid() + } + + case Apply(fun @ Select(receiver, _), args) => + fun.symbol.getAnnotation(LinkTimePropertyAnnotation) match { + case Some(annotation) => + val propName = annotation.constantAtIndex(0).get.stringValue + js.LinkTimeProperty(propName)(toIRType(tree.tpe)) + + case None if isPrimitive(fun.symbol) => + val code = getPrimitive(fun.symbol) + + 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 JS code for a primitive JS call (to a method of a subclass of js.Any) * This is the typed Scala.js to JS bridge feature. Basically it boils * down to calling the method without name mangling. But other aspects @@ -7292,37 +7369,6 @@ private object GenJSCode { private val ObjectArgConstructorName = MethodName.constructor(List(jswkn.ObjectRef)) - private val lengthMethodName = - MethodName("length", Nil, jstpe.IntRef) - private val charAtMethodName = - MethodName("charAt", List(jstpe.IntRef), jstpe.CharRef) - - private val getNameMethodName = - MethodName("getName", Nil, jstpe.ClassRef(jswkn.BoxedStringClass)) - private val isPrimitiveMethodName = - MethodName("isPrimitive", Nil, jstpe.BooleanRef) - private val isInterfaceMethodName = - MethodName("isInterface", Nil, jstpe.BooleanRef) - private val isArrayMethodName = - MethodName("isArray", Nil, jstpe.BooleanRef) - private val getComponentTypeMethodName = - MethodName("getComponentType", Nil, jstpe.ClassRef(jswkn.ClassClass)) - private val getSuperclassMethodName = - MethodName("getSuperclass", Nil, jstpe.ClassRef(jswkn.ClassClass)) - - private val isInstanceMethodName = - MethodName("isInstance", List(jstpe.ClassRef(jswkn.ObjectClass)), jstpe.BooleanRef) - private val isAssignableFromMethodName = - MethodName("isAssignableFrom", List(jstpe.ClassRef(jswkn.ClassClass)), jstpe.BooleanRef) - private val castMethodName = - MethodName("cast", List(jstpe.ClassRef(jswkn.ObjectClass)), jstpe.ClassRef(jswkn.ObjectClass)) - - private val arrayNewInstanceMethodName = { - MethodName("newInstance", - List(jstpe.ClassRef(jswkn.ClassClass), jstpe.IntRef), - jstpe.ClassRef(jswkn.ObjectClass)) - } - private val thisOriginalName = OriginalName("this") private object BlockOrAlone { @@ -7338,4 +7384,145 @@ private object GenJSCode { case _ => Some((tree, Nil)) } } + + private object IntFlipSign { + def unapply(tree: js.Tree): Option[js.Tree] = tree match { + case js.BinaryOp(js.BinaryOp.Int_^, lhs, js.IntLiteral(Int.MinValue)) => + Some(lhs) + case js.BinaryOp(js.BinaryOp.Int_^, js.IntLiteral(Int.MinValue), rhs) => + Some(rhs) + case _ => + None + } + } + + private object LongFlipSign { + def unapply(tree: js.Tree): Option[js.Tree] = tree match { + case js.BinaryOp(js.BinaryOp.Long_^, lhs, js.LongLiteral(Long.MinValue)) => + Some(lhs) + case js.BinaryOp(js.BinaryOp.Long_^, js.LongLiteral(Long.MinValue), rhs) => + Some(rhs) + case _ => + None + } + } + + private abstract class JavalibOpBody { + /** Generates the body of this special method, given references to the receiver and parameters. */ + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree + } + + private object JavalibOpBody { + private def checkNotNullIf(arg: js.Tree, checkNulls: Boolean)(implicit pos: ir.Position): js.Tree = + if (checkNulls && arg.tpe.isNullable) js.UnaryOp(js.UnaryOp.CheckNotNull, arg) + else arg + + /* These are case classes for convenience (for the apply method). + * They are not intended for pattern matching. + */ + + /** UnaryOp applying to the `this` parameter. */ + final case class ThisUnaryOp(op: js.UnaryOp.Code) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + assert(args.isEmpty) + js.UnaryOp(op, receiver) + } + } + + /** BinaryOp applying to the `this` parameter and the regular parameter. */ + final case class ThisBinaryOp(op: js.BinaryOp.Code, checkNulls: Boolean = false) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + val List(rhs) = args: @unchecked + js.BinaryOp(op, receiver, checkNotNullIf(rhs, checkNulls)) + } + } + + /** UnaryOp applying to the only regular parameter (`this` is ignored). */ + final case class ArgUnaryOp(op: js.UnaryOp.Code, checkNulls: Boolean = false) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + val List(arg) = args: @unchecked + js.UnaryOp(op, checkNotNullIf(arg, checkNulls)) + } + } + + /** BinaryOp applying to the two regular paramters (`this` is ignored). */ + final case class ArgBinaryOp(op: js.BinaryOp.Code, checkNulls: Boolean = false) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + val List(lhs, rhs) = args: @unchecked + js.BinaryOp(op, checkNotNullIf(lhs, checkNulls), checkNotNullIf(rhs, checkNulls)) + } + } + } + + /** Methods of the javalib whose body must be replaced by a dedicated + * UnaryOp or BinaryOp. + * + * We use IR encoded names to identify them, rather than scalac Symbols. + * There is no fundamental reason for that. It makes it easier to define + * this map in a declarative way, especially when overloaded methods are + * concerned (Array.newInstance). It also allows to define it independently + * of the Global instance, but that is marginal. + */ + private lazy val JavalibMethodsWithOpBody: Map[(ClassName, MethodName), JavalibOpBody] = { + import JavalibOpBody._ + import js.{UnaryOp => unop, BinaryOp => binop} + import jstpe.{BooleanRef => Z, CharRef => C, IntRef => I, LongRef => J, FloatRef => F, DoubleRef => D} + import MethodName.{apply => m} + + val O = jswkn.ObjectRef + val CC = jstpe.ClassRef(jswkn.ClassClass) + val T = jstpe.ClassRef(jswkn.BoxedStringClass) + + val byClass: Map[ClassName, Map[MethodName, JavalibOpBody]] = Map( + jswkn.BoxedIntegerClass.withSuffix("$") -> Map( + m("toUnsignedLong", List(I), J) -> ArgUnaryOp(unop.UnsignedIntToLong), + m("divideUnsigned", List(I, I), I) -> ArgBinaryOp(binop.Int_unsigned_/), + m("remainderUnsigned", List(I, I), I) -> ArgBinaryOp(binop.Int_unsigned_%), + m("numberOfLeadingZeros", List(I), I) -> ArgUnaryOp(unop.Int_clz) + ), + jswkn.BoxedLongClass.withSuffix("$") -> Map( + m("divideUnsigned", List(J, J), J) -> ArgBinaryOp(binop.Long_unsigned_/), + m("remainderUnsigned", List(J, J), J) -> ArgBinaryOp(binop.Long_unsigned_%), + m("numberOfLeadingZeros", List(J), I) -> ArgUnaryOp(unop.Long_clz) + ), + jswkn.BoxedFloatClass.withSuffix("$") -> Map( + m("floatToIntBits", List(F), I) -> ArgUnaryOp(unop.Float_toBits), + m("intBitsToFloat", List(I), F) -> ArgUnaryOp(unop.Float_fromBits) + ), + jswkn.BoxedDoubleClass.withSuffix("$") -> Map( + m("doubleToLongBits", List(D), J) -> ArgUnaryOp(unop.Double_toBits), + m("longBitsToDouble", List(J), D) -> ArgUnaryOp(unop.Double_fromBits) + ), + jswkn.BoxedStringClass -> Map( + m("length", Nil, I) -> ThisUnaryOp(unop.String_length), + m("charAt", List(I), C) -> ThisBinaryOp(binop.String_charAt) + ), + jswkn.ClassClass -> Map( + // Unary operators + m("getName", Nil, T) -> ThisUnaryOp(unop.Class_name), + m("isPrimitive", Nil, Z) -> ThisUnaryOp(unop.Class_isPrimitive), + m("isInterface", Nil, Z) -> ThisUnaryOp(unop.Class_isInterface), + m("isArray", Nil, Z) -> ThisUnaryOp(unop.Class_isArray), + m("getComponentType", Nil, CC) -> ThisUnaryOp(unop.Class_componentType), + m("getSuperclass", Nil, CC) -> ThisUnaryOp(unop.Class_superClass), + // Binary operators + m("isInstance", List(O), Z) -> ThisBinaryOp(binop.Class_isInstance), + m("isAssignableFrom", List(CC), Z) -> ThisBinaryOp(binop.Class_isAssignableFrom, checkNulls = true), + m("cast", List(O), O) -> ThisBinaryOp(binop.Class_cast) + ), + ClassName("java.lang.System$") -> Map( + m("identityHashCode", List(O), I) -> ArgUnaryOp(unop.IdentityHashCode) + ), + ClassName("java.lang.reflect.Array$") -> Map( + m("newInstance", List(CC, I), O) -> ArgBinaryOp(binop.Class_newArray, checkNulls = true) + ) + ) + + for { + (cls, methods) <- byClass + (methodName, body) <- methods + } yield { + (cls, methodName) -> body + } + } } diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala index 2b0c5590d9..e91b74d4ff 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala @@ -131,14 +131,16 @@ trait JSDefinitions { lazy val Runtime_withContextualJSClassValue = getMemberMethod(RuntimePackageModule, newTermName("withContextualJSClassValue")) lazy val Runtime_privateFieldsSymbol = getMemberMethod(RuntimePackageModule, newTermName("privateFieldsSymbol")) lazy val Runtime_linkingInfo = getMemberMethod(RuntimePackageModule, newTermName("linkingInfo")) - lazy val Runtime_identityHashCode = getMemberMethod(RuntimePackageModule, newTermName("identityHashCode")) lazy val Runtime_dynamicImport = getMemberMethod(RuntimePackageModule, newTermName("dynamicImport")) lazy val LinkingInfoModule = getRequiredModule("scala.scalajs.LinkingInfo") + lazy val LinkingInfo_linkTimeIf = getMemberMethod(LinkingInfoModule, newTermName("linkTimeIf")) lazy val LinkingInfo_linkTimePropertyBoolean = getMemberMethod(LinkingInfoModule, newTermName("linkTimePropertyBoolean")) lazy val LinkingInfo_linkTimePropertyInt = getMemberMethod(LinkingInfoModule, newTermName("linkTimePropertyInt")) lazy val LinkingInfo_linkTimePropertyString = getMemberMethod(LinkingInfoModule, newTermName("linkTimePropertyString")) + lazy val LinkTimePropertyAnnotation = getRequiredClass("scala.scalajs.annotation.linkTimeProperty") + lazy val DynamicImportThunkClass = getRequiredClass("scala.scalajs.runtime.DynamicImportThunk") lazy val DynamicImportThunkClass_apply = getMemberMethod(DynamicImportThunkClass, nme.apply) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala index 90aa1b1513..a199b87f98 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala @@ -58,8 +58,7 @@ abstract class JSPrimitives { final val CREATE_INNER_JS_CLASS = CONSTRUCTOROF + 1 // runtime.createInnerJSClass final val CREATE_LOCAL_JS_CLASS = CREATE_INNER_JS_CLASS + 1 // runtime.createLocalJSClass final val WITH_CONTEXTUAL_JS_CLASS_VALUE = CREATE_LOCAL_JS_CLASS + 1 // runtime.withContextualJSClassValue - final val IDENTITY_HASH_CODE = WITH_CONTEXTUAL_JS_CLASS_VALUE + 1 // runtime.identityHashCode - final val DYNAMIC_IMPORT = IDENTITY_HASH_CODE + 1 // runtime.dynamicImport + final val DYNAMIC_IMPORT = WITH_CONTEXTUAL_JS_CLASS_VALUE + 1 // runtime.dynamicImport final val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals final val IN = STRICT_EQ + 1 // js.special.in @@ -71,7 +70,8 @@ abstract class JSPrimitives { final val WRAP_AS_THROWABLE = JS_TRY_CATCH + 1 // js.special.wrapAsThrowable final val UNWRAP_FROM_THROWABLE = WRAP_AS_THROWABLE + 1 // js.special.unwrapFromThrowable final val DEBUGGER = UNWRAP_FROM_THROWABLE + 1 // js.special.debugger - final val LINKTIME_PROPERTY = DEBUGGER + 1 // LinkingInfo.linkTimePropertyXXX + final val LINKTIME_IF = DEBUGGER + 1 // LinkingInfo.linkTimeIf + final val LINKTIME_PROPERTY = LINKTIME_IF + 1 // LinkingInfo.linkTimePropertyXXX final val LastJSPrimitiveCode = LINKTIME_PROPERTY @@ -114,7 +114,6 @@ abstract class JSPrimitives { addPrimitive(Runtime_createLocalJSClass, CREATE_LOCAL_JS_CLASS) addPrimitive(Runtime_withContextualJSClassValue, WITH_CONTEXTUAL_JS_CLASS_VALUE) - addPrimitive(Runtime_identityHashCode, IDENTITY_HASH_CODE) addPrimitive(Runtime_dynamicImport, DYNAMIC_IMPORT) addPrimitive(Special_strictEquals, STRICT_EQ) @@ -128,6 +127,7 @@ abstract class JSPrimitives { addPrimitive(Special_unwrapFromThrowable, UNWRAP_FROM_THROWABLE) addPrimitive(Special_debugger, DEBUGGER) + addPrimitive(LinkingInfo_linkTimeIf, LINKTIME_IF) addPrimitive(LinkingInfo_linkTimePropertyBoolean, LINKTIME_PROPERTY) addPrimitive(LinkingInfo_linkTimePropertyInt, LINKTIME_PROPERTY) addPrimitive(LinkingInfo_linkTimePropertyString, LINKTIME_PROPERTY) diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/LinkTimeIfTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/LinkTimeIfTest.scala new file mode 100644 index 0000000000..881c0e9a2f --- /dev/null +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/LinkTimeIfTest.scala @@ -0,0 +1,109 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.nscplugin.test + +import util._ + +import org.junit.Test +import org.junit.Assert._ + +// scalastyle:off line.size.limit + +class LinkTimeIfTest extends TestHelpers { + override def preamble: String = "import scala.scalajs.LinkingInfo._" + + private final val IllegalLinkTimeIfArgMessage = { + "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." + } + + @Test + def linkTimeErrorInvalidOp(): Unit = { + """ + object A { + def foo = + linkTimeIf((esVersion + 1) < ESVersion.ES2015) { } { } + } + """ hasErrors + s""" + |newSource1.scala:4: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf((esVersion + 1) < ESVersion.ES2015) { } { } + | ^ + """ + } + + @Test + def linkTimeErrorInvalidEntities(): Unit = { + """ + object A { + def foo(x: String) = { + val bar = 1 + linkTimeIf(bar == 0) { } { } + } + } + """ hasErrors + s""" + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(bar == 0) { } { } + | ^ + """ + + // String comparison is a `BinaryOp.===`, which is not allowed + """ + object A { + def foo(x: String) = + linkTimeIf("foo" == x) { } { } + } + """ hasErrors + s""" + |newSource1.scala:4: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf("foo" == x) { } { } + | ^ + """ + + """ + object A { + def bar = true + def foo(x: String) = + linkTimeIf(bar || !bar) { } { } + } + """ hasErrors + s""" + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(bar || !bar) { } { } + | ^ + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(bar || !bar) { } { } + | ^ + """ + } + + @Test + def linkTimeCondInvalidTree(): Unit = { + """ + object A { + def bar = true + def foo(x: String) = + linkTimeIf(if (bar) true else false) { } { } + } + """ hasErrors + s""" + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(if (bar) true else false) { } { } + | ^ + """ + } +} diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala index b10bef4b95..f38e2adf28 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala @@ -582,6 +582,72 @@ class OptimizationTest extends JSASTTest { case js.LoadModule(`testName`) => } } + + @Test + def unsignedComparisonsInt: Unit = { + import js.BinaryOp._ + + val comparisons = List( + (Int_unsigned_<, "<"), + (Int_unsigned_<=, "<="), + (Int_unsigned_>, ">"), + (Int_unsigned_>=, ">=") + ) + + for ((op, codeOp) <- comparisons) { + s""" + class Test { + private final val SignBit = Int.MinValue + + def unsignedComparisonsInt(x: Int, y: Int): Unit = { + (x ^ 0x80000000) $codeOp (y ^ 0x80000000) + (SignBit ^ x) $codeOp (y ^ SignBit) + (SignBit ^ x) $codeOp 0x80000010 + 0x00000020 $codeOp (y ^ SignBit) + } + } + """.hasExactly(4, "unsigned comparisons") { + case js.BinaryOp(`op`, _, _) => + }.hasNot("any Int_^") { + case js.BinaryOp(Int_^, _, _) => + }.hasNot("any signed comparison") { + case js.BinaryOp(Int_< | Int_<= | Int_> | Int_>=, _, _) => + } + } + } + + @Test + def unsignedComparisonsLong: Unit = { + import js.BinaryOp._ + + val comparisons = List( + (Long_unsigned_<, "<"), + (Long_unsigned_<=, "<="), + (Long_unsigned_>, ">"), + (Long_unsigned_>=, ">=") + ) + + for ((op, codeOp) <- comparisons) { + s""" + class Test { + private final val SignBit = Long.MinValue + + def unsignedComparisonsInt(x: Long, y: Long): Unit = { + (x ^ 0x8000000000000000L) $codeOp (y ^ 0x8000000000000000L) + (SignBit ^ x) $codeOp (y ^ SignBit) + (SignBit ^ x) $codeOp 0x8000000000000010L + 0x0000000000000020L $codeOp (y ^ SignBit) + } + } + """.hasExactly(4, "unsigned comparisons") { + case js.BinaryOp(`op`, _, _) => + }.hasNot("any Long_^") { + case js.BinaryOp(Long_^, _, _) => + }.hasNot("any signed comparison") { + case js.BinaryOp(Long_< | Long_<= | Long_> | Long_>=, _, _) => + } + } + } } object OptimizationTest { diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala index ad94d65549..599e9e8c1c 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -206,6 +206,13 @@ object Hashers { mixTree(elsep) mixType(tree.tpe) + case LinkTimeIf(cond, thenp, elsep) => + mixTag(TagLinkTimeIf) + mixTree(cond) + mixTree(thenp) + mixTree(elsep) + mixType(tree.tpe) + case While(cond, body) => mixTag(TagWhile) mixTree(cond) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala index c69ad1447c..216bec733e 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -93,6 +93,7 @@ object Printers { protected def printBlock(tree: Tree): Unit = { val trees = tree match { case Block(trees) => trees + case Skip() => Nil case _ => tree :: Nil } printBlock(trees) @@ -232,6 +233,14 @@ object Printers { printBlock(elsep) } + case LinkTimeIf(cond, thenp, elsep) => + print("link-time-if (") + print(cond) + print(") ") + printBlock(thenp) + print(" else ") + printBlock(elsep) + case While(cond, body) => print("while (") print(cond) @@ -436,6 +445,16 @@ object Printers { case UnwrapFromThrowable => p("(", ")") case Throw => p("throw ", "") + + case Float_toBits => p("(", ")") + case Float_fromBits => p("(", ")") + case Double_toBits => p("(", ")") + case Double_fromBits => p("(", ")") + + case Int_clz => p("(", ")") + case Long_clz => p("(", ")") + + case UnsignedIntToLong => p("(", ")") } case BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) => @@ -562,6 +581,21 @@ object Printers { case Double_<= => "<=[double]" case Double_> => ">[double]" case Double_>= => ">=[double]" + + case Int_unsigned_/ => "unsigned_/[int]" + case Int_unsigned_% => "unsigned_%[int]" + case Long_unsigned_/ => "unsigned_/[long]" + case Long_unsigned_% => "unsigned_%[long]" + + case Int_unsigned_< => "unsigned_<[int]" + case Int_unsigned_<= => "unsigned_<=[int]" + case Int_unsigned_> => "unsigned_>[int]" + case Int_unsigned_>= => "unsigned_>=[int]" + + case Long_unsigned_< => "unsigned_<[long]" + case Long_unsigned_<= => "unsigned_<=[long]" + case Long_unsigned_> => "unsigned_>[long]" + case Long_unsigned_>= => "unsigned_>=[long]" }) print(' ') print(rhs) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index 7ad9ee3876..23292cbcdc 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,8 +17,8 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.19.0", - binaryEmitted = "1.19" + current = "1.20.0-SNAPSHOT", + binaryEmitted = "1.20-SNAPSHOT" ) /** Helper class to allow for testing of logic. */ diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala index 7cc64e28e1..628630dfa1 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -297,6 +297,11 @@ object Serializers { writeTree(cond); writeTree(thenp); writeTree(elsep) writeType(tree.tpe) + case LinkTimeIf(cond, thenp, elsep) => + writeTagAndPos(TagLinkTimeIf) + writeTree(cond); writeTree(thenp); writeTree(elsep) + writeType(tree.tpe) + case While(cond, body) => writeTagAndPos(TagWhile) writeTree(cond); writeTree(body) @@ -1196,9 +1201,14 @@ object Serializers { Assign(lhs.asInstanceOf[AssignLhs], rhs) - case TagReturn => Return(readTree(), readLabelName()) - case TagIf => If(readTree(), readTree(), readTree())(readType()) - case TagWhile => While(readTree(), readTree()) + case TagReturn => + Return(readTree(), readLabelName()) + case TagIf => + If(readTree(), readTree(), readTree())(readType()) + case TagLinkTimeIf => + LinkTimeIf(readTree(), readTree(), readTree())(readType()) + case TagWhile => + While(readTree(), readTree()) case TagDoWhile => if (!hacks.useBelow(13)) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala b/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala index bc7d2982b0..dc2862b7ec 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala @@ -135,6 +135,9 @@ private[ir] object Tags { final val TagNewLambda = TagApplyTypedClosure + 1 final val TagJSAwait = TagNewLambda + 1 + // New in 1.20 + final val TagLinkTimeIf = TagJSAwait + 1 + // Tags for member defs final val TagFieldDef = 1 diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala index 27d9086435..e95a154e1c 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -60,6 +60,9 @@ object Transformers { case If(cond, thenp, elsep) => If(transform(cond), transform(thenp), transform(elsep))(tree.tpe) + case LinkTimeIf(cond, thenp, elsep) => + LinkTimeIf(transform(cond), transform(thenp), transform(elsep))(tree.tpe) + case While(cond, body) => While(transform(cond), transform(body)) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala index d5782da074..15c9da9093 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -48,6 +48,11 @@ object Traversers { traverse(thenp) traverse(elsep) + case LinkTimeIf(cond, thenp, elsep) => + traverse(cond) + traverse(thenp) + traverse(elsep) + case While(cond, body) => traverse(cond) traverse(body) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index ccc3b56196..ca2c76dcc8 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -168,6 +168,38 @@ object Trees { sealed case class If(cond: Tree, thenp: Tree, elsep: Tree)(val tpe: Type)( implicit val pos: Position) extends Tree + /** Link-time `if` expression. + * + * The `cond` must be a well-typed link-time tree of type `boolean`. + * + * A link-time tree is a `Tree` matching the following sub-grammar: + * + * {{{ + * link-time-tree ::= + * BooleanLiteral + * | IntLiteral + * | StringLiteral + * | LinkTimeProperty + * | UnaryOp(link-time-unary-op, link-time-tree) + * | BinaryOp(link-time-binary-op, link-time-tree, link-time-tree) + * | LinkTimeIf(link-time-tree, link-time-tree, link-time-tree) + * + * link-time-unary-op ::= + * Boolean_! + * + * link-time-binary-op ::= + * Boolean_== | Boolean_!= | Boolean_| | Boolean_& + * | Int_== | Int_!= | Int_< | Int_<= | Int_> | Int_>= + * }}} + * + * Note: nested `LinkTimeIf` nodes in the `cond` are used to encode + * short-circuiting boolean `&&` and `||`, just like we do with regular + * `If` nodes. + */ + sealed case class LinkTimeIf(cond: Tree, thenp: Tree, elsep: Tree)( + val tpe: Type)(implicit val pos: Position) + extends Tree + sealed case class While(cond: Tree, body: Tree)( implicit val pos: Position) extends Tree { val tpe = cond match { @@ -477,6 +509,19 @@ object Trees { final val UnwrapFromThrowable = 30 final val Throw = 31 + // Floating point bit manipulation, introduced in 1.20 + final val Float_toBits = 32 + // final val Float_toRawBits = 33 // Reserved + final val Float_fromBits = 34 + final val Double_toBits = 35 + // final val Double_toRawBits = 36 // Reserved + final val Double_fromBits = 37 + + // Other nodes introduced in 1.20 + final val Int_clz = 38 + final val Long_clz = 39 + final val UnsignedIntToLong = 40 + def isClassOp(op: Code): Boolean = op >= Class_name && op <= Class_superClass @@ -498,13 +543,14 @@ object Trees { case IntToShort => ShortType case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | - String_length | Array_length | IdentityHashCode => + String_length | Array_length | IdentityHashCode | Float_toBits | + Int_clz | Long_clz => IntType - case IntToLong | DoubleToLong => + case IntToLong | DoubleToLong | Double_toBits | UnsignedIntToLong => LongType - case DoubleToFloat | LongToFloat => + case DoubleToFloat | LongToFloat | Float_fromBits => FloatType - case IntToDouble | LongToDouble | FloatToDouble => + case IntToDouble | LongToDouble | FloatToDouble | Double_fromBits => DoubleType case CheckNotNull | Clone => argType.toNonNullable @@ -639,6 +685,23 @@ object Trees { final val Class_cast = 61 final val Class_newArray = 62 + // New in 1.20 + + final val Int_unsigned_/ = 63 + final val Int_unsigned_% = 64 + final val Long_unsigned_/ = 65 + final val Long_unsigned_% = 66 + + final val Int_unsigned_< = 67 + final val Int_unsigned_<= = 68 + final val Int_unsigned_> = 69 + final val Int_unsigned_>= = 70 + + final val Long_unsigned_< = 71 + final val Long_unsigned_<= = 72 + final val Long_unsigned_> = 73 + final val Long_unsigned_>= = 74 + def isClassOp(op: Code): Boolean = op >= Class_isInstance && op <= Class_newArray @@ -648,15 +711,19 @@ object Trees { Int_== | Int_!= | Int_< | Int_<= | Int_> | Int_>= | Long_== | Long_!= | Long_< | Long_<= | Long_> | Long_>= | Double_== | Double_!= | Double_< | Double_<= | Double_> | Double_>= | - Class_isInstance | Class_isAssignableFrom => + Class_isInstance | Class_isAssignableFrom | + Int_unsigned_< | Int_unsigned_<= | Int_unsigned_> | Int_unsigned_>= | + Long_unsigned_< | Long_unsigned_<= | Long_unsigned_> | Long_unsigned_>= => BooleanType case String_+ => StringType case Int_+ | Int_- | Int_* | Int_/ | Int_% | - Int_| | Int_& | Int_^ | Int_<< | Int_>>> | Int_>> => + Int_| | Int_& | Int_^ | Int_<< | Int_>>> | Int_>> | + Int_unsigned_/ | Int_unsigned_% => IntType case Long_+ | Long_- | Long_* | Long_/ | Long_% | - Long_| | Long_& | Long_^ | Long_<< | Long_>>> | Long_>> => + Long_| | Long_& | Long_^ | Long_<< | Long_>>> | Long_>> | + Long_unsigned_/ | Long_unsigned_% => LongType case Float_+ | Float_- | Float_* | Float_/ | Float_% => FloatType diff --git a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala index 060bf4fdb8..ea68b14864 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -202,6 +202,61 @@ class PrintersTest { If(ref("x", BooleanType), ref("y", BooleanType), b(false))(BooleanType)) } + @Test def printLinkTimeIf(): Unit = { + assertPrintEquals( + """ + |link-time-if (true) { + | 5 + |} else { + | 6 + |} + """, + LinkTimeIf(b(true), i(5), i(6))(IntType)) + + assertPrintEquals( + """ + |link-time-if (true) { + | 5 + |} else { + |} + """, + LinkTimeIf(b(true), i(5), Skip())(VoidType)) + + assertPrintEquals( + """ + |link-time-if (true) { + | 5 + |} else { + | link-time-if (false) { + | 6 + | } else { + | 7 + | } + |} + """, + LinkTimeIf(b(true), i(5), LinkTimeIf(b(false), i(6), i(7))(IntType))(IntType)) + + assertPrintEquals( + """ + |link-time-if (x) { + | true + |} else { + | y + |} + """, + LinkTimeIf(ref("x", BooleanType), b(true), ref("y", BooleanType))(BooleanType)) + + assertPrintEquals( + """ + |link-time-if (x) { + | y + |} else { + | false + |} + """, + LinkTimeIf(ref("x", BooleanType), ref("y", BooleanType), b(false))(BooleanType)) + } + @Test def printWhile(): Unit = { assertPrintEquals( """ @@ -459,6 +514,16 @@ class PrintersTest { assertPrintEquals("(e)", UnaryOp(WrapAsThrowable, ref("e", AnyType))) assertPrintEquals("(e)", UnaryOp(UnwrapFromThrowable, ref("e", ClassType(ThrowableClass, nullable = true)))) + + assertPrintEquals("(x)", UnaryOp(Float_toBits, ref("x", FloatType))) + assertPrintEquals("(x)", UnaryOp(Float_fromBits, ref("x", IntType))) + assertPrintEquals("(x)", UnaryOp(Double_toBits, ref("x", DoubleType))) + assertPrintEquals("(x)", UnaryOp(Double_fromBits, ref("x", LongType))) + + assertPrintEquals("(x)", UnaryOp(Int_clz, ref("x", IntType))) + assertPrintEquals("(x)", UnaryOp(Long_clz, ref("x", LongType))) + + assertPrintEquals("(x)", UnaryOp(UnsignedIntToLong, ref("x", IntType))) } @Test def printPseudoUnaryOp(): Unit = { @@ -611,6 +676,33 @@ class PrintersTest { BinaryOp(Class_isAssignableFrom, classVarRef, ref("y", ClassType(ClassClass, nullable = false)))) assertPrintEquals("cast(x, y)", BinaryOp(Class_cast, classVarRef, ref("y", AnyType))) assertPrintEquals("newArray(x, y)", BinaryOp(Class_newArray, classVarRef, ref("y", IntType))) + + assertPrintEquals("(x unsigned_/[int] y)", + BinaryOp(Int_unsigned_/, ref("x", IntType), ref("y", IntType))) + assertPrintEquals("(x unsigned_%[int] y)", + BinaryOp(Int_unsigned_%, ref("x", IntType), ref("y", IntType))) + assertPrintEquals("(x unsigned_/[long] y)", + BinaryOp(Long_unsigned_/, ref("x", LongType), ref("y", LongType))) + assertPrintEquals("(x unsigned_%[long] y)", + BinaryOp(Long_unsigned_%, ref("x", LongType), ref("y", LongType))) + + assertPrintEquals("(x unsigned_<[int] y)", + BinaryOp(Int_unsigned_<, ref("x", IntType), ref("y", IntType))) + assertPrintEquals("(x unsigned_<=[int] y)", + BinaryOp(Int_unsigned_<=, ref("x", IntType), ref("y", IntType))) + assertPrintEquals("(x unsigned_>[int] y)", + BinaryOp(Int_unsigned_>, ref("x", IntType), ref("y", IntType))) + assertPrintEquals("(x unsigned_>=[int] y)", + BinaryOp(Int_unsigned_>=, ref("x", IntType), ref("y", IntType))) + + assertPrintEquals("(x unsigned_<[long] y)", + BinaryOp(Long_unsigned_<, ref("x", LongType), ref("y", LongType))) + assertPrintEquals("(x unsigned_<=[long] y)", + BinaryOp(Long_unsigned_<=, ref("x", LongType), ref("y", LongType))) + assertPrintEquals("(x unsigned_>[long] y)", + BinaryOp(Long_unsigned_>, ref("x", LongType), ref("y", LongType))) + assertPrintEquals("(x unsigned_>=[long] y)", + BinaryOp(Long_unsigned_>=, ref("x", LongType), ref("y", LongType))) } @Test def printNewArray(): Unit = { diff --git a/javalib/src/main/scala/java/lang/Double.scala b/javalib/src/main/scala/java/lang/Double.scala index aa6e3bc8d9..b8c1ffc779 100644 --- a/javalib/src/main/scala/java/lang/Double.scala +++ b/javalib/src/main/scala/java/lang/Double.scala @@ -364,14 +364,28 @@ object Double { @inline def isFinite(d: scala.Double): scala.Boolean = !isNaN(d) && !isInfinite(d) + /** Hash code of a number (excluding Longs). + * + * Because of the common encoding for integer and floating point values, + * the hashCode of Floats and Doubles must align with that of Ints for the + * common values. + * + * For other values, we use the hashCode specified by the JavaDoc for + * *Doubles*, even for values which are valid Float values. Because of the + * previous point, we cannot align completely with the Java specification, + * so there is no point trying to be a bit more aligned here. Always using + * the Double version requires fewer branches. + * + * We use different code paths in JS and Wasm for performance reasons. + * The two implementations compute the same results. + */ @inline def hashCode(value: scala.Double): Int = { if (LinkingInfo.isWebAssembly) hashCodeForWasm(value) else - FloatingPointBits.numberHashCode(value) + hashCodeForJS(value) } - // See FloatingPointBits for the spec of this computation @inline private def hashCodeForWasm(value: scala.Double): Int = { val bits = doubleToLongBits(value) @@ -382,13 +396,20 @@ object Double { Long.hashCode(bits) } - // Wasm intrinsic + @inline + private def hashCodeForJS(value: scala.Double): Int = { + val valueInt = (value.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int] + if (valueInt.toDouble == value && 1.0/value != scala.Double.NegativeInfinity) + valueInt + else + Long.hashCode(doubleToLongBits(value)) + } + @inline def longBitsToDouble(bits: scala.Long): scala.Double = - FloatingPointBits.longBitsToDouble(bits) + throw new Error("stub") // body replaced by the compiler back-end - // Wasm intrinsic @inline def doubleToLongBits(value: scala.Double): scala.Long = - FloatingPointBits.doubleToLongBits(value) + throw new Error("stub") // body replaced by the compiler back-end @inline def sum(a: scala.Double, b: scala.Double): scala.Double = a + b diff --git a/javalib/src/main/scala/java/lang/Float.scala b/javalib/src/main/scala/java/lang/Float.scala index 8fa4ce3070..279d8ed1a8 100644 --- a/javalib/src/main/scala/java/lang/Float.scala +++ b/javalib/src/main/scala/java/lang/Float.scala @@ -13,9 +13,9 @@ package java.lang import java.lang.constant.{Constable, ConstantDesc} -import java.math.BigInteger import scala.scalajs.js +import scala.scalajs.LinkingInfo._ /* This is a hijacked class. Its instances are primitive numbers. * Constructors are not emitted. @@ -226,9 +226,23 @@ object Float { fractionalPartStr: String, exponentStr: String, zDown: scala.Float, zUp: scala.Float, mid: scala.Double): scala.Float = { + /* Get the best available implementation of big integers for the given platform. + * + * If JS bigint's are supported, use them. Otherwise fall back on + * `java.math.BigInteger`. + * + * We need a `linkTimeIf` here because the JS bigint implementation uses + * the `**` operator, which does not link when `esVersion < ESVersion.ES2016`. + */ + val bigIntImpl = linkTimeIf[BigIntImpl](esVersion >= ESVersion.ES2020) { + BigIntImpl.JSBigInt + } { + BigIntImpl.JBigInteger + } + // 1. Accurately parse the string with the representation f × 10ᵉ - val f: BigInteger = new BigInteger(integralPartStr + fractionalPartStr) + val f: bigIntImpl.Repr = bigIntImpl.fromString(integralPartStr + fractionalPartStr) val e: Int = Integer.parseInt(exponentStr) - fractionalPartStr.length() /* Note: we know that `e` is "reasonable" (in the range [-324, +308]). If @@ -261,24 +275,23 @@ object Float { val mExplicitBits = midBits & ((1L << mbits) - 1) val mImplicit1Bit = 1L << mbits // the implicit '1' bit of a normalized floating-point number - val m = BigInteger.valueOf(mExplicitBits | mImplicit1Bit) + val m = bigIntImpl.fromUnsignedLong53(mExplicitBits | mImplicit1Bit) val k = biasedK - bias - mbits // 3. Accurately compare f × 10ᵉ to m × 2ᵏ - @inline def compare(x: BigInteger, y: BigInteger): Int = - x.compareTo(y) + import bigIntImpl.{multiplyBy2Pow, multiplyBy10Pow} val cmp = if (e >= 0) { if (k >= 0) - compare(multiplyBy10Pow(f, e), multiplyBy2Pow(m, k)) + bigIntImpl.compare(multiplyBy10Pow(f, e), multiplyBy2Pow(m, k)) else - compare(multiplyBy2Pow(multiplyBy10Pow(f, e), -k), m) // this branch may be dead code in practice + bigIntImpl.compare(multiplyBy2Pow(multiplyBy10Pow(f, e), -k), m) // this branch may be dead code in practice } else { if (k >= 0) - compare(f, multiplyBy2Pow(multiplyBy10Pow(m, -e), k)) + bigIntImpl.compare(f, multiplyBy2Pow(multiplyBy10Pow(m, -e), k)) else - compare(multiplyBy2Pow(f, -k), multiplyBy10Pow(m, -e)) + bigIntImpl.compare(multiplyBy2Pow(f, -k), multiplyBy10Pow(m, -e)) } // 4. Choose zDown or zUp depending on the result of the comparison @@ -293,11 +306,54 @@ object Float { zUp } - @inline private def multiplyBy10Pow(v: BigInteger, e: Int): BigInteger = - v.multiply(BigInteger.TEN.pow(e)) + /** An implementation of big integer arithmetics that we need in the above method. */ + private sealed abstract class BigIntImpl { + type Repr + + def fromString(str: String): Repr + + /** Creates a big integer from a `Long` that needs at most 53 bits (unsigned). */ + def fromUnsignedLong53(x: scala.Long): Repr + + def multiplyBy2Pow(v: Repr, e: Int): Repr + def multiplyBy10Pow(v: Repr, e: Int): Repr + + def compare(x: Repr, y: Repr): Int + } + + private object BigIntImpl { + object JSBigInt extends BigIntImpl { + type Repr = js.BigInt + + @inline def fromString(str: String): Repr = js.BigInt(str) - @inline private def multiplyBy2Pow(v: BigInteger, e: Int): BigInteger = - v.shiftLeft(e) + // The 53-bit restriction guarantees that the conversion to `Double` is lossless. + @inline def fromUnsignedLong53(x: scala.Long): Repr = js.BigInt(x.toDouble) + + @inline def multiplyBy2Pow(v: Repr, e: Int): Repr = v << js.BigInt(e) + @inline def multiplyBy10Pow(v: Repr, e: Int): Repr = v * (js.BigInt(10) ** js.BigInt(e)) + + @inline def compare(x: Repr, y: Repr): Int = { + if (x < y) -1 + else if (x > y) 1 + else 0 + } + } + + object JBigInteger extends BigIntImpl { + import java.math.BigInteger + + type Repr = BigInteger + + @inline def fromString(str: String): Repr = new BigInteger(str) + @inline def fromUnsignedLong53(x: scala.Long): Repr = BigInteger.valueOf(x) + + @inline def multiplyBy2Pow(v: Repr, e: Int): Repr = v.shiftLeft(e) + @inline def multiplyBy10Pow(v: Repr, e: Int): Repr = v.multiply(BigInteger.TEN.pow(e)) + + @inline def compare(x: Repr, y: Repr): Int = x.compareTo(y) + } + } private def parseFloatHexadecimal(integralPartStr: String, fractionalPartStr: String, binaryExpStr: String): scala.Float = { @@ -371,13 +427,11 @@ object Float { @inline def hashCode(value: scala.Float): Int = Double.hashCode(value.toDouble) - // Wasm intrinsic @inline def intBitsToFloat(bits: scala.Int): scala.Float = - FloatingPointBits.intBitsToFloat(bits) + throw new Error("stub") // body replaced by the compiler back-end - // Wasm intrinsic @inline def floatToIntBits(value: scala.Float): scala.Int = - FloatingPointBits.floatToIntBits(value) + throw new Error("stub") // body replaced by the compiler back-end @inline def sum(a: scala.Float, b: scala.Float): scala.Float = a + b diff --git a/javalib/src/main/scala/java/lang/FloatingPointBits.scala b/javalib/src/main/scala/java/lang/FloatingPointBits.scala deleted file mode 100644 index 96e1c8f64c..0000000000 --- a/javalib/src/main/scala/java/lang/FloatingPointBits.scala +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Scala.js (https://www.scala-js.org/) - * - * Copyright EPFL. - * - * Licensed under Apache License 2.0 - * (https://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -package java.lang - -import scala.scalajs.js -import scala.scalajs.js.Dynamic.global -import scala.scalajs.js.typedarray -import scala.scalajs.LinkingInfo.ESVersion - -/** Manipulating the bits of floating point numbers. */ -private[lang] object FloatingPointBits { - - import scala.scalajs.LinkingInfo - - private[this] val _areTypedArraysSupported = { - // Here we use the `esVersion` test to dce the 4 subsequent tests - LinkingInfo.esVersion >= ESVersion.ES2015 || { - js.typeOf(global.ArrayBuffer) != "undefined" && - js.typeOf(global.Int32Array) != "undefined" && - js.typeOf(global.Float32Array) != "undefined" && - js.typeOf(global.Float64Array) != "undefined" - } - } - - @inline - private def areTypedArraysSupported: scala.Boolean = { - /* We have a forwarder to the internal `val _areTypedArraysSupported` to - * be able to inline it. This achieves the following: - * * If we emit ES2015+, dce `|| _areTypedArraysSupported` and replace - * `areTypedArraysSupported` by `true` in the calling code, allowing - * polyfills in the calling code to be dce'ed in turn. - * * If we emit ES5, replace `areTypedArraysSupported` by - * `_areTypedArraysSupported` so we do not calculate it multiple times. - */ - LinkingInfo.esVersion >= ESVersion.ES2015 || _areTypedArraysSupported - } - - private val arrayBuffer = - if (areTypedArraysSupported) new typedarray.ArrayBuffer(8) - else null - - private val int32Array = - if (areTypedArraysSupported) new typedarray.Int32Array(arrayBuffer, 0, 2) - else null - - private val float32Array = - if (areTypedArraysSupported) new typedarray.Float32Array(arrayBuffer, 0, 2) - else null - - private val float64Array = - if (areTypedArraysSupported) new typedarray.Float64Array(arrayBuffer, 0, 1) - else null - - private val areTypedArraysBigEndian = { - if (areTypedArraysSupported) { - int32Array(0) = 0x01020304 - (new typedarray.Int8Array(arrayBuffer, 0, 8))(0) == 0x01 - } else { - true // as good a value as any - } - } - - private val highOffset = if (areTypedArraysBigEndian) 0 else 1 - private val lowOffset = if (areTypedArraysBigEndian) 1 else 0 - - private val floatPowsOf2: js.Array[scala.Double] = - if (areTypedArraysSupported) null - else makePowsOf2(len = 1 << 8, java.lang.Float.MIN_NORMAL.toDouble) - - private val doublePowsOf2: js.Array[scala.Double] = - if (areTypedArraysSupported) null - else makePowsOf2(len = 1 << 11, java.lang.Double.MIN_NORMAL) - - private def makePowsOf2(len: Int, minNormal: scala.Double): js.Array[scala.Double] = { - val r = new js.Array[scala.Double](len) - r(0) = 0.0 - var i = 1 - var next = minNormal - while (i != len - 1) { - r(i) = next - i += 1 - next *= 2 - } - r(len - 1) = scala.Double.PositiveInfinity - r - } - - /** Hash code of a number (excluding Longs). - * - * Because of the common encoding for integer and floating point values, - * the hashCode of Floats and Doubles must align with that of Ints for the - * common values. - * - * For other values, we use the hashCode specified by the JavaDoc for - * *Doubles*, even for values which are valid Float values. Because of the - * previous point, we cannot align completely with the Java specification, - * so there is no point trying to be a bit more aligned here. Always using - * the Double version should typically be faster on VMs without fround - * support because we avoid several fround operations. - */ - def numberHashCode(value: scala.Double): Int = { - val iv = rawToInt(value) - if (iv == value && 1.0/value != scala.Double.NegativeInfinity) { - iv - } else { - /* Basically an inlined version of `Long.hashCode(doubleToLongBits(value))`, - * so that we never allocate a RuntimeLong instance (or anything, for - * that matter). - * - * In addition, in the happy path where typed arrays are supported, since - * we xor together the two Ints, it doesn't matter which one comes first - * or second, and hence we can use constants 0 and 1 instead of having an - * indirection through `highOffset` and `lowOffset`. - */ - if (areTypedArraysSupported) { - float64Array(0) = value - int32Array(0) ^ int32Array(1) - } else { - doubleHashCodePolyfill(value) - } - } - } - - @noinline - private def doubleHashCodePolyfill(value: scala.Double): Int = - Long.hashCode(doubleToLongBitsPolyfillInline(value)) - - def intBitsToFloat(bits: Int): scala.Float = { - if (areTypedArraysSupported) { - int32Array(0) = bits - float32Array(0) - } else { - intBitsToFloatPolyfill(bits).toFloat - } - } - - def floatToIntBits(value: scala.Float): Int = { - if (areTypedArraysSupported) { - float32Array(0) = value - int32Array(0) - } else { - floatToIntBitsPolyfill(value) - } - } - - def longBitsToDouble(bits: scala.Long): scala.Double = { - if (areTypedArraysSupported) { - int32Array(highOffset) = (bits >>> 32).toInt - int32Array(lowOffset) = bits.toInt - float64Array(0) - } else { - longBitsToDoublePolyfill(bits) - } - } - - def doubleToLongBits(value: scala.Double): scala.Long = { - if (areTypedArraysSupported) { - float64Array(0) = value - ((int32Array(highOffset).toLong << 32) | - (int32Array(lowOffset).toLong & 0xffffffffL)) - } else { - doubleToLongBitsPolyfill(value) - } - } - - /* --- Polyfills for floating point bit manipulations --- - * - * Originally inspired by - * https://github.com/inexorabletash/polyfill/blob/3447582628b6e3ea81959c4d5987aa332c22d1ca/typedarray.js#L150-L264 - * - * Note that if typed arrays are not supported, it is almost certain that - * fround is not supported natively, so Float operations are extremely slow. - * - * We therefore do all computations in Doubles here. - */ - - private def intBitsToFloatPolyfill(bits: Int): scala.Double = { - val ebits = 8 - val fbits = 23 - val sign = (bits >> 31) | 1 // -1 or 1 - val e = (bits >> fbits) & ((1 << ebits) - 1) - val f = bits & ((1 << fbits) - 1) - decodeIEEE754(ebits, fbits, floatPowsOf2, scala.Float.MinPositiveValue, sign, e, f) - } - - private def floatToIntBitsPolyfill(floatValue: scala.Float): Int = { - // Some constants - val ebits = 8 - val fbits = 23 - - // Force computations to be on Doubles - val value = floatValue.toDouble - - // Determine sign bit and compute the absolute value av - val sign = if (value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)) -1 else 1 - val s = sign & scala.Int.MinValue - val av = sign * value - - // Compute e and f - val powsOf2 = this.floatPowsOf2 // local cache - val e = encodeIEEE754Exponent(ebits, powsOf2, av) - val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, scala.Float.MinPositiveValue.toDouble, av, e) - - // Encode - s | (e << fbits) | rawToInt(f) - } - - private def longBitsToDoublePolyfill(bits: scala.Long): scala.Double = { - val ebits = 11 - val fbits = 52 - val hifbits = fbits - 32 - val hi = (bits >>> 32).toInt - val lo = Utils.toUint(bits.toInt) - val sign = (hi >> 31) | 1 // -1 or 1 - val e = (hi >> hifbits) & ((1 << ebits) - 1) - val f = (hi & ((1 << hifbits) - 1)).toDouble * 0x100000000L.toDouble + lo - decodeIEEE754(ebits, fbits, doublePowsOf2, scala.Double.MinPositiveValue, sign, e, f) - } - - @noinline - private def doubleToLongBitsPolyfill(value: scala.Double): scala.Long = - doubleToLongBitsPolyfillInline(value) - - @inline - private def doubleToLongBitsPolyfillInline(value: scala.Double): scala.Long = { - // Some constants - val ebits = 11 - val fbits = 52 - val hifbits = fbits - 32 - - // Determine sign bit and compute the absolute value av - val sign = if (value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)) -1 else 1 - val s = sign & scala.Int.MinValue - val av = sign * value - - // Compute e and f - val powsOf2 = this.doublePowsOf2 // local cache - val e = encodeIEEE754Exponent(ebits, powsOf2, av) - val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, scala.Double.MinPositiveValue, av, e) - - // Encode - val hi = s | (e << hifbits) | rawToInt(f / 0x100000000L.toDouble) - val lo = rawToInt(f) - (hi.toLong << 32) | (lo.toLong & 0xffffffffL) - } - - @inline - private def decodeIEEE754(ebits: Int, fbits: Int, - powsOf2: js.Array[scala.Double], minPositiveValue: scala.Double, - sign: scala.Int, e: Int, f: scala.Double): scala.Double = { - - // Some constants - val specialExponent = (1 << ebits) - 1 - val twoPowFbits = (1L << fbits).toDouble - - if (e == specialExponent) { - // Special - if (f == 0.0) - sign * scala.Double.PositiveInfinity - else - scala.Double.NaN - } else if (e > 0) { - // Normalized - sign * powsOf2(e) * (1 + f / twoPowFbits) - } else { - // Subnormal - sign * f * minPositiveValue - } - } - - private def encodeIEEE754Exponent(ebits: Int, - powsOf2: js.Array[scala.Double], av: scala.Double): Int = { - - /* Binary search of `av` inside `powsOf2`. - * There are exactly `ebits` iterations of this loop (11 for Double, 8 for Float). - */ - var eMin = 0 - var eMax = 1 << ebits - while (eMin + 1 < eMax) { - val e = (eMin + eMax) >> 1 - if (av < powsOf2(e)) // false when av is NaN - eMax = e - else - eMin = e - } - eMin - } - - @inline - private def encodeIEEE754MantissaBits(ebits: Int, fbits: Int, - powsOf2: js.Array[scala.Double], minPositiveValue: scala.Double, - av: scala.Double, e: Int): scala.Double = { - - // Some constants - val specialExponent = (1 << ebits) - 1 - val twoPowFbits = (1L << fbits).toDouble - - if (e == specialExponent) { - if (av != av) - (1L << (fbits - 1)).toDouble // NaN - else - 0.0 // Infinity - } else { - if (e == 0) - av / minPositiveValue // Subnormal - else - ((av / powsOf2(e)) - 1.0) * twoPowFbits // Normal - } - } - - @inline private def rawToInt(x: scala.Double): Int = { - import scala.scalajs.js.DynamicImplicits.number2dynamic - (x | 0).asInstanceOf[Int] - } - -} diff --git a/javalib/src/main/scala/java/lang/Integer.scala b/javalib/src/main/scala/java/lang/Integer.scala index a4c2694365..f817acfd67 100644 --- a/javalib/src/main/scala/java/lang/Integer.scala +++ b/javalib/src/main/scala/java/lang/Integer.scala @@ -186,18 +186,20 @@ object Integer { parse(s, base) } - @inline def compare(x: scala.Int, y: scala.Int): scala.Int = - if (x == y) 0 else if (x < y) -1 else 1 + @inline def compare(x: scala.Int, y: scala.Int): scala.Int = { + if (x == y) 0 + else if (x < y) -1 + else 1 + } @inline def compareUnsigned(x: scala.Int, y: scala.Int): scala.Int = { - import Utils.toUint if (x == y) 0 - else if (toUint(x) > toUint(y)) 1 - else -1 + else if ((x ^ Int.MinValue) < (y ^ Int.MinValue)) -1 + else 1 } @inline def toUnsignedLong(x: Int): scala.Long = - x.toLong & 0xffffffffL + throw new Error("stub") // body replaced by the compiler back-end // Wasm intrinsic def bitCount(i: scala.Int): scala.Int = { @@ -221,15 +223,11 @@ object Integer { (((t2 + (t2 >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24 } - // Wasm intrinsic @inline def divideUnsigned(dividend: Int, divisor: Int): Int = - if (divisor == 0) 0 / 0 - else asInt(asUint(dividend) / asUint(divisor)) + throw new Error("stub") // body replaced by the compiler back-end - // Wasm intrinsic @inline def remainderUnsigned(dividend: Int, divisor: Int): Int = - if (divisor == 0) 0 % 0 - else asInt(asUint(dividend) % asUint(divisor)) + throw new Error("stub") // body replaced by the compiler back-end @inline def highestOneBit(i: Int): Int = { /* The natural way of implementing this is: @@ -278,30 +276,8 @@ object Integer { @inline def signum(i: scala.Int): scala.Int = if (i == 0) 0 else if (i < 0) -1 else 1 - // Intrinsic, fallback on actual code for non-literal in JS - @inline def numberOfLeadingZeros(i: scala.Int): scala.Int = { - if (LinkingInfo.esVersion >= ESVersion.ES2015) js.Math.clz32(i) - else clz32Dynamic(i) - } - - private def clz32Dynamic(i: scala.Int) = { - if (js.typeOf(js.Dynamic.global.Math.clz32) == "function") { - js.Math.clz32(i) - } else { - // See Hacker's Delight, Section 5-3 - var x = i - if (x == 0) { - 32 - } else { - var r = 1 - if ((x & 0xffff0000) == 0) { x <<= 16; r += 16 } - if ((x & 0xff000000) == 0) { x <<= 8; r += 8 } - if ((x & 0xf0000000) == 0) { x <<= 4; r += 4 } - if ((x & 0xc0000000) == 0) { x <<= 2; r += 2 } - r + (x >> 31) - } - } - } + @inline def numberOfLeadingZeros(i: scala.Int): scala.Int = + throw new Error("stub") // body replaced by the compiler back-end // Wasm intrinsic @inline def numberOfTrailingZeros(i: scala.Int): scala.Int = diff --git a/javalib/src/main/scala/java/lang/Long.scala b/javalib/src/main/scala/java/lang/Long.scala index 0413372acf..41f54dc685 100644 --- a/javalib/src/main/scala/java/lang/Long.scala +++ b/javalib/src/main/scala/java/lang/Long.scala @@ -15,6 +15,8 @@ package java.lang import scala.annotation.{switch, tailrec} import java.lang.constant.{Constable, ConstantDesc} +import java.lang.Utils.toUint +import java.util.ScalaOps._ import scala.scalajs.js @@ -65,33 +67,39 @@ object Long { private final val SignBit = scala.Long.MinValue + /** Quantities in this class are interpreted as unsigned values. */ private final class StringRadixInfo(val chunkLength: Int, - val radixPowLength: scala.Long, val paddingZeros: String, - val overflowBarrier: scala.Long) + val radixPowLength: Int, val radixPowLengthInverse: scala.Double, + val paddingZeros: String, val overflowBarrier: scala.Long) /** Precomputed table for toUnsignedStringInternalLarge and * parseUnsignedLongInternal. */ private lazy val StringRadixInfos: js.Array[StringRadixInfo] = { val r = new js.Array[StringRadixInfo]() - var radix = 0 - while (radix < Character.MIN_RADIX) { + for (radix <- 0 until Character.MIN_RADIX) r.push(null) - radix += 1 - } - while (radix <= Character.MAX_RADIX) { + for (radix <- Character.MIN_RADIX to Character.MAX_RADIX) { /* Find the biggest chunk size we can use. * - * - radixPowLength should be the biggest signed int32 value that is an - * exact power of radix. + * - radixPowLength should be the biggest exact power of radix that is <= 2^30. * - chunkLength is then log_radix(radixPowLength). * - paddingZeros is a string with exactly chunkLength '0's. * - overflowBarrier is divideUnsigned(-1L, radixPowLength) so that we * can test whether someValue * radixPowLength will overflow. + * + * It holds that 2^30 >= radixPowLength > 2^30 / maxRadix = 2^30 / 36 > 2^24. + * + * Therefore, (2^64 - 1) / radixPowLength < 2^(64-24) = 2^40, which + * comfortably fits in a `Double`. `toUnsignedStringLarge` relies on that + * property. + * + * Also, radixPowLength² < 2^63, and radixPowLength³ > 2^64. + * `parseUnsignedLongInternal` relies on that property. */ - val barrier = Int.MaxValue / radix + val barrier = Integer.divideUnsigned(1 << 30, radix) var radixPowLength = radix var chunkLength = 1 var paddingZeros = "0" @@ -100,11 +108,9 @@ object Long { chunkLength += 1 paddingZeros += "0" } - val radixPowLengthLong = radixPowLength.toLong - val overflowBarrier = Long.divideUnsigned(-1L, radixPowLengthLong) - r.push(new StringRadixInfo(chunkLength, radixPowLengthLong, - paddingZeros, overflowBarrier)) - radix += 1 + val overflowBarrier = Long.divideUnsigned(-1L, Integer.toUnsignedLong(radixPowLength)) + r.push(new StringRadixInfo(chunkLength, radixPowLength, + 1.0 / radixPowLength.toDouble, paddingZeros, overflowBarrier)) } r @@ -142,50 +148,78 @@ object Long { private def toStringImpl(i: scala.Long, radix: Int): String = { val lo = i.toInt val hi = (i >>> 32).toInt + if (lo >> 31 == hi) { // It's a signed int32 import js.JSNumberOps.enableJSNumberOps lo.toString(radix) } else if (hi < 0) { - "-" + toUnsignedStringInternalLarge(-i, radix) + val neg = -i + "-" + toUnsignedStringInternalLarge(neg.toInt, (neg >>> 32).toInt, radix) } else { - toUnsignedStringInternalLarge(i, radix) + toUnsignedStringInternalLarge(lo, hi, radix) } } // Must be called only with valid radix private def toUnsignedStringImpl(i: scala.Long, radix: Int): String = { - if ((i >>> 32).toInt == 0) { + val lo = i.toInt + val hi = (i >>> 32).toInt + + if (hi == 0) { // It's an unsigned int32 import js.JSNumberOps.enableJSNumberOps - Utils.toUint(i.toInt).toString(radix) + Utils.toUint(lo).toString(radix) } else { - toUnsignedStringInternalLarge(i, radix) + toUnsignedStringInternalLarge(lo, hi, radix) } } - // Must be called only with valid radix - private def toUnsignedStringInternalLarge(i: scala.Long, radix: Int): String = { + // Must be called only with valid radix and with (lo, hi) >= 2^30 + private def toUnsignedStringInternalLarge(lo: Int, hi: Int, radix: Int): String = { import js.JSNumberOps.enableJSNumberOps import js.JSStringOps.enableJSStringOps - val radixInfo = StringRadixInfos(radix) - val divisor = radixInfo.radixPowLength - val paddingZeros = radixInfo.paddingZeros + @inline def unsignedSafeDoubleLo(n: scala.Double): Int = { + import js.DynamicImplicits.number2dynamic + (n | 0).asInstanceOf[Int] + } - val divisorXorSignBit = divisor.toLong ^ SignBit + val TwoPow32 = (1L << 32).toDouble + val approxNum = toUint(hi) * TwoPow32 + toUint(lo) - var res = "" - var value = i - while ((value ^ SignBit) >= divisorXorSignBit) { // unsigned comparison - val div = divideUnsigned(value, divisor) - val rem = value - div * divisor // == remainderUnsigned(value, divisor) - val remStr = rem.toInt.toString(radix) - res = paddingZeros.jsSubstring(remStr.length) + remStr + res - value = div - } + if ((hi & 0xffe00000) == 0) { // see RuntimeLong.isUnsignedSafeDouble + // (lo, hi) is small enough to be a Double, so approxNum is exact + approxNum.toString(radix) + } else { + /* See RuntimeLong.toUnsignedString for a proof. Although that proof is + * done in terms of a fixed divisor of 10^9, it generalizes to any + * divisor that statisfies 2^12 < divisor <= 2^30 and + * ULong.MaxValue / divisor < 2^53, which is true for `radixPowLength`. + */ - value.toInt.toString(radix) + res + val radixInfo = StringRadixInfos(radix) + val divisor = radixInfo.radixPowLength + val divisorInv = radixInfo.radixPowLengthInverse + val paddingZeros = radixInfo.paddingZeros + + // initial approximation of the quotient and remainder + var approxQuot = Math.floor(approxNum * divisorInv) + var approxRem = lo - divisor * unsignedSafeDoubleLo(approxQuot) + + // correct the approximations + if (approxRem < 0) { + approxQuot -= 1.0 + approxRem += divisor + } else if (approxRem >= divisor) { + approxQuot += 1.0 + approxRem -= divisor + } + + // build the result string + val remStr = approxRem.toString(radix) + approxQuot.toString(radix) + paddingZeros.jsSubstring(remStr.length) + remStr + } } def parseLong(s: String, radix: Int): scala.Long = { @@ -291,7 +325,7 @@ object Long { firstResult } else { // Second chunk. Still cannot overflow. - val multiplier = radixInfo.radixPowLength + val multiplier = Integer.toUnsignedLong(radixInfo.radixPowLength) val secondChunkEnd = firstChunkEnd + chunkLen val secondResult = firstResult * multiplier + parseChunk(firstChunkEnd, secondChunkEnd) @@ -337,59 +371,26 @@ object Long { @inline def hashCode(value: scala.Long): Int = value.toInt ^ (value >>> 32).toInt - // Intrinsic + // RuntimeLong intrinsic @inline def compare(x: scala.Long, y: scala.Long): scala.Int = { if (x == y) 0 else if (x < y) -1 else 1 } - // TODO Intrinsic? - @inline def compareUnsigned(x: scala.Long, y: scala.Long): scala.Int = - compare(x ^ SignBit, y ^ SignBit) - - // Intrinsic, except for JS when using bigint's for longs - def divideUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = - divModUnsigned(dividend, divisor, isDivide = true) - - // Intrinsic, except for JS when using bigint's for longs - def remainderUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = - divModUnsigned(dividend, divisor, isDivide = false) - - private def divModUnsigned(a: scala.Long, b: scala.Long, - isDivide: scala.Boolean): scala.Long = { - /* This is a much simplified (and slow) version of - * RuntimeLong.unsignedDivModHelper. - */ - - if (b == 0L) - throw new ArithmeticException("/ by zero") - - var shift = numberOfLeadingZeros(b) - numberOfLeadingZeros(a) - var bShift = b << shift - - var rem = a - var quot = 0L - - /* Invariants: - * bShift == b << shift == b * 2^shift - * quot >= 0 - * 0 <= rem < 2 * bShift - * quot * b + rem == a - */ - while (shift >= 0 && rem != 0) { - if ((rem ^ SignBit) >= (bShift ^ SignBit)) { - rem -= bShift - quot |= (1L << shift) - } - shift -= 1 - bShift >>>= 1 - } - - if (isDivide) quot - else rem + // TODO RuntimeLong intrinsic? + @inline def compareUnsigned(x: scala.Long, y: scala.Long): scala.Int = { + if (x == y) 0 + else if ((x ^ scala.Long.MinValue) < (y ^ scala.Long.MinValue)) -1 + else 1 } + @inline def divideUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = + throw new Error("stub") // body replaced by the compiler back-end + + @inline def remainderUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = + throw new Error("stub") // body replaced by the compiler back-end + @inline def highestOneBit(i: scala.Long): scala.Long = { val lo = i.toInt @@ -455,13 +456,9 @@ object Long { else 1 } - // Wasm intrinsic @inline - def numberOfLeadingZeros(l: scala.Long): Int = { - val hi = (l >>> 32).toInt - if (hi != 0) Integer.numberOfLeadingZeros(hi) - else Integer.numberOfLeadingZeros(l.toInt) + 32 - } + def numberOfLeadingZeros(l: scala.Long): Int = + throw new Error("stub") // body replaced by the compiler back-end // Wasm intrinsic @inline diff --git a/javalib/src/main/scala/java/lang/Math.scala b/javalib/src/main/scala/java/lang/Math.scala index 7d77391990..7fe386b7a7 100644 --- a/javalib/src/main/scala/java/lang/Math.scala +++ b/javalib/src/main/scala/java/lang/Math.scala @@ -462,6 +462,43 @@ object Math { if (a >= Integer.MIN_VALUE && a <= Integer.MAX_VALUE) a.toInt else throw new ArithmeticException("Integer overflow") + // RuntimeLong intrinsic + @inline + def multiplyFull(x: scala.Int, y: scala.Int): scala.Long = + x.toLong * y.toLong + + @inline + def multiplyHigh(x: scala.Long, y: scala.Long): scala.Long = { + /* Hacker's Delight, Section 8-2, Figure 8-2, + * where we have "inlined" all the variables used only once to help our + * optimizer perform simplifications. + */ + + val x0 = x & 0xffffffffL + val x1 = x >> 32 + val y0 = y & 0xffffffffL + val y1 = y >> 32 + + val t = x1 * y0 + ((x0 * y0) >>> 32) + x1 * y1 + (t >> 32) + (((t & 0xffffffffL) + x0 * y1) >> 32) + } + + @inline + def unsignedMultiplyHigh(x: scala.Long, y: scala.Long): scala.Long = { + /* Hacker's Delight, Section 8-2: + * > For an unsigned version, simply change all the int declarations to unsigned. + * In Scala, that means changing all the >> into >>>. + */ + + val x0 = x & 0xffffffffL + val x1 = x >>> 32 + val y0 = y & 0xffffffffL + val y1 = y >>> 32 + + val t = x1 * y0 + ((x0 * y0) >>> 32) + x1 * y1 + (t >>> 32) + (((t & 0xffffffffL) + x0 * y1) >>> 32) + } + def floorDiv(a: scala.Int, b: scala.Int): scala.Int = { val quot = a / b if ((a < 0) == (b < 0) || quot * b == a) quot diff --git a/javalib/src/main/scala/java/lang/System.scala b/javalib/src/main/scala/java/lang/System.scala index 976ea7ff15..d6ee87996f 100644 --- a/javalib/src/main/scala/java/lang/System.scala +++ b/javalib/src/main/scala/java/lang/System.scala @@ -184,7 +184,7 @@ object System { @inline def identityHashCode(x: Any): scala.Int = - scala.scalajs.runtime.identityHashCode(x.asInstanceOf[AnyRef]) + throw new Error("stub") // body replaced by the compiler back-end // System properties -------------------------------------------------------- diff --git a/javalib/src/main/scala/java/util/ArrayDeque.scala b/javalib/src/main/scala/java/util/ArrayDeque.scala index b45e075d03..53a048a46c 100644 --- a/javalib/src/main/scala/java/util/ArrayDeque.scala +++ b/javalib/src/main/scala/java/util/ArrayDeque.scala @@ -17,15 +17,11 @@ import java.lang.Utils._ import java.util.ScalaOps._ -import scala.scalajs.js - class ArrayDeque[E] private (initialCapacity: Int) extends AbstractCollection[E] with Deque[E] with Cloneable with Serializable { self => - private val inner: js.Array[E] = new js.Array[E](Math.max(initialCapacity, 16)) - - fillNulls(0, inner.length) + private var inner: Array[AnyRef] = new Array[AnyRef](Math.max(initialCapacity, 16)) private var status = 0 private var startIndex = 0 // inclusive, 0 <= startIndex < inner.length @@ -56,7 +52,7 @@ class ArrayDeque[E] private (initialCapacity: Int) startIndex -= 1 if (startIndex < 0) startIndex = inner.length - 1 - inner(startIndex) = e + inner(startIndex) = e.asInstanceOf[AnyRef] status += 1 empty = false true @@ -71,7 +67,7 @@ class ArrayDeque[E] private (initialCapacity: Int) endIndex += 1 if (endIndex > inner.length) endIndex = 1 - inner(endIndex - 1) = e + inner(endIndex - 1) = e.asInstanceOf[AnyRef] status += 1 empty = false true @@ -95,8 +91,8 @@ class ArrayDeque[E] private (initialCapacity: Int) def pollFirst(): E = { if (isEmpty()) null.asInstanceOf[E] else { - val res = inner(startIndex) - inner(startIndex) = null.asInstanceOf[E] // free reference for GC + val res = inner(startIndex).asInstanceOf[E] + inner(startIndex) = null // free reference for GC startIndex += 1 if (startIndex == endIndex) empty = true @@ -111,8 +107,8 @@ class ArrayDeque[E] private (initialCapacity: Int) if (isEmpty()) { null.asInstanceOf[E] } else { - val res = inner(endIndex - 1) - inner(endIndex - 1) = null.asInstanceOf[E] // free reference for GC + val res = inner(endIndex - 1).asInstanceOf[E] + inner(endIndex - 1) = null // free reference for GC endIndex -= 1 if (startIndex == endIndex) empty = true @@ -139,12 +135,12 @@ class ArrayDeque[E] private (initialCapacity: Int) def peekFirst(): E = { if (isEmpty()) null.asInstanceOf[E] - else inner(startIndex) + else inner(startIndex).asInstanceOf[E] } def peekLast(): E = { if (isEmpty()) null.asInstanceOf[E] - else inner(endIndex - 1) + else inner(endIndex - 1).asInstanceOf[E] } def removeFirstOccurrence(o: Any): Boolean = { @@ -222,7 +218,7 @@ class ArrayDeque[E] private (initialCapacity: Int) else if (nextIndex >= inner.length) nextIndex = 0 - inner(lastIndex) + inner(lastIndex).asInstanceOf[E] } override def remove(): Unit = { @@ -278,7 +274,7 @@ class ArrayDeque[E] private (initialCapacity: Int) nextIndex = inner.length - 1 } - inner(lastIndex) + inner(lastIndex).asInstanceOf[E] } override def remove(): Unit = { @@ -358,20 +354,14 @@ class ArrayDeque[E] private (initialCapacity: Int) // Nothing to do (constructor ensures capacity is always non-zero). } else if (startIndex == 0 && endIndex == inner.length) { val oldCapacity = inner.length - inner.length *= 2 - // no copying required: We just keep adding to the end. - // However, ensure array is dense. - fillNulls(oldCapacity, inner.length) + // No moving required within the array; we grow only at the end. + inner = Arrays.copyOf(inner, oldCapacity * 2) } else if (startIndex == endIndex) { val oldCapacity = inner.length - inner.length *= 2 // move beginning of array to end - for (i <- 0 until endIndex) { - inner(i + oldCapacity) = inner(i) - inner(i) = null.asInstanceOf[E] // free old reference for GC - } - // ensure rest of array is dense - fillNulls(endIndex + oldCapacity, inner.length) + val newArr = new Array[AnyRef](oldCapacity * 2) + System.arraycopy(inner, 0, newArr, oldCapacity, endIndex) + inner = newArr endIndex += oldCapacity } } @@ -398,9 +388,8 @@ class ArrayDeque[E] private (initialCapacity: Int) true } else if (target < endIndex) { // Shift elements from endIndex towards target - for (i <- target until endIndex - 1) - inner(i) = inner(i + 1) - inner(endIndex - 1) = null.asInstanceOf[E] // free reference for GC + System.arraycopy(inner, target + 1, inner, target, endIndex - (target + 1)) + inner(endIndex - 1) = null // free reference for GC status += 1 /* Note that endIndex >= 2: @@ -429,13 +418,8 @@ class ArrayDeque[E] private (initialCapacity: Int) * ==> contradiction. */ - // for (i <- target until startIndex by -1) - var i = target - while (i != startIndex) { - inner(i) = inner(i - 1) - i -= 1 - } - inner(startIndex) = null.asInstanceOf[E] // free reference for GC + System.arraycopy(inner, startIndex, inner, startIndex + 1, target - startIndex) + inner(startIndex) = null // free reference for GC status += 1 @@ -451,9 +435,4 @@ class ArrayDeque[E] private (initialCapacity: Int) false } } - - private def fillNulls(from: Int, until: Int): Unit = { - for (i <- from until until) - inner(i) = null.asInstanceOf[E] - } } diff --git a/javalib/src/main/scala/java/util/ArrayList.scala b/javalib/src/main/scala/java/util/ArrayList.scala index 68b9705f62..1c67de682b 100644 --- a/javalib/src/main/scala/java/util/ArrayList.scala +++ b/javalib/src/main/scala/java/util/ArrayList.scala @@ -14,80 +14,181 @@ package java.util import java.lang.Cloneable import java.lang.Utils._ +import java.util.ScalaOps._ import scala.scalajs._ +import scala.scalajs.LinkingInfo.isWebAssembly -class ArrayList[E] private (private[ArrayList] val inner: js.Array[E]) +class ArrayList[E] private (innerInit: AnyRef, private var _size: Int) extends AbstractList[E] with RandomAccess with Cloneable with Serializable { self => + /* This class has two different implementations for handling the + * internal data storage, depending on whether we are on Wasm or JS. + * On JS, we utilize `js.Array`. On Wasm, for performance reasons, + * we avoid JS interop and use a scala.Array. + * The `_size` field (unused in JS) keeps track of the effective size + * of the underlying Array for the Wasm implementation. + */ + + private val innerJS: js.Array[E] = + if (isWebAssembly) null + else innerInit.asInstanceOf[js.Array[E]] + + private var innerWasm: Array[AnyRef] = + if (!isWebAssembly) null + else innerInit.asInstanceOf[Array[AnyRef]] + def this(initialCapacity: Int) = { - this(new js.Array[E]) - if (initialCapacity < 0) - throw new IllegalArgumentException + this( + { + if (initialCapacity < 0) + throw new IllegalArgumentException + if (isWebAssembly) new Array[AnyRef](initialCapacity) + else new js.Array[E] + }, + 0 + ) } - def this() = - this(new js.Array[E]) + def this() = this(16) def this(c: Collection[_ <: E]) = { - this() + this(c.size()) addAll(c) } def trimToSize(): Unit = { - // We ignore this as js.Array doesn't support explicit pre-allocation + if (isWebAssembly) + resizeTo(size()) + // We ignore this in JS as js.Array doesn't support explicit pre-allocation } def ensureCapacity(minCapacity: Int): Unit = { - // We ignore this as js.Array doesn't support explicit pre-allocation + if (isWebAssembly) { + if (innerWasm.length < minCapacity) { + if (minCapacity > (1 << 30)) + resizeTo(minCapacity) + else + resizeTo(((1 << 31) >>> (Integer.numberOfLeadingZeros(minCapacity - 1)) - 1)) + } + } + // We ignore this in JS as js.Array doesn't support explicit pre-allocation } def size(): Int = - inner.length - - override def clone(): AnyRef = - new ArrayList(inner.jsSlice(0)) + if (isWebAssembly) _size + else innerJS.length + + override def clone(): AnyRef = { + if (isWebAssembly) + new ArrayList(innerWasm.clone(), size()) + else + new ArrayList(innerJS.jsSlice(0), 0) + } def get(index: Int): E = { checkIndexInBounds(index) - inner(index) + if (isWebAssembly) + innerWasm(index).asInstanceOf[E] + else + innerJS(index) } override def set(index: Int, element: E): E = { val e = get(index) - inner(index) = element + if (isWebAssembly) + innerWasm(index) = element.asInstanceOf[AnyRef] + else + innerJS(index) = element e } override def add(e: E): Boolean = { - inner.push(e) + if (isWebAssembly) { + if (size() >= innerWasm.length) + expand() + innerWasm(size()) = e.asInstanceOf[AnyRef] + _size += 1 + } else { + innerJS.push(e) + } true } override def add(index: Int, element: E): Unit = { checkIndexOnBounds(index) - inner.splice(index, 0, element) + if (isWebAssembly) { + if (size() >= innerWasm.length) + expand() + System.arraycopy(innerWasm, index, innerWasm, index + 1, size() - index) + innerWasm(index) = element.asInstanceOf[AnyRef] + _size += 1 + } else { + innerJS.splice(index, 0, element) + } } override def remove(index: Int): E = { checkIndexInBounds(index) - arrayRemoveAndGet(inner, index) + if (isWebAssembly) { + val removed = innerWasm(index).asInstanceOf[E] + System.arraycopy(innerWasm, index + 1, innerWasm, index, size() - index - 1) + innerWasm(size - 1) = null // free reference for GC + _size -= 1 + removed + } else { + arrayRemoveAndGet(innerJS, index) + } } override def clear(): Unit = - inner.length = 0 + if (isWebAssembly) { + Arrays.fill(innerWasm, null) // free references for GC + _size = 0 + } else { + innerJS.length = 0 + } override def addAll(index: Int, c: Collection[_ <: E]): Boolean = { c match { case other: ArrayList[_] => - inner.splice(index, 0, other.inner.toSeq: _*) + checkIndexOnBounds(index) + if (isWebAssembly) { + ensureCapacity(size() + other.size()) + System.arraycopy(innerWasm, index, innerWasm, index + other.size(), size() - index) + System.arraycopy(other.innerWasm, 0, innerWasm, index, other.size()) + _size += c.size() + } else { + innerJS.splice(index, 0, other.innerJS.toSeq: _*) + } other.size() > 0 case _ => super.addAll(index, c) } } - override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = - inner.splice(fromIndex, toIndex - fromIndex) + override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = { + if (fromIndex < 0 || toIndex > size() || toIndex < fromIndex) + throw new IndexOutOfBoundsException() + if (isWebAssembly) { + if (fromIndex != toIndex) { + System.arraycopy(innerWasm, toIndex, innerWasm, fromIndex, size() - toIndex) + val newSize = size() - toIndex + fromIndex + Arrays.fill(innerWasm, newSize, size(), null) // free references for GC + _size = newSize + } + } else { + innerJS.splice(fromIndex, toIndex - fromIndex) + } + } + // Wasm only + private def expand(): Unit = { + resizeTo(Math.max(innerWasm.length * 2, 16)) + } + + // Wasm only + private def resizeTo(newCapacity: Int): Unit = { + innerWasm = Arrays.copyOf(innerWasm, newCapacity) + } } diff --git a/library/src/main/scala/scala/scalajs/LinkingInfo.scala b/library/src/main/scala/scala/scalajs/LinkingInfo.scala index ea9d6c1a2f..0a7218fb44 100644 --- a/library/src/main/scala/scala/scalajs/LinkingInfo.scala +++ b/library/src/main/scala/scala/scalajs/LinkingInfo.scala @@ -12,6 +12,8 @@ package scala.scalajs +import scala.scalajs.annotation.linkTimeProperty + object LinkingInfo { /** Returns true if we are linking for production, false otherwise. @@ -42,7 +44,7 @@ object LinkingInfo { * * @see [[developmentMode]] */ - @inline + @inline @linkTimeProperty("core/productionMode") def productionMode: Boolean = linkTimePropertyBoolean("core/productionMode") @@ -120,7 +122,7 @@ object LinkingInfo { * useES2018Feature() * }}} */ - @inline + @inline @linkTimeProperty("core/esVersion") def esVersion: Int = linkTimePropertyInt("core/esVersion") @@ -218,7 +220,7 @@ object LinkingInfo { * implementationWithoutES2015Semantics() * }}} */ - @inline + @inline @linkTimeProperty("core/useECMAScript2015Semantics") def useECMAScript2015Semantics: Boolean = linkTimePropertyBoolean("core/useECMAScript2015Semantics") @@ -252,15 +254,50 @@ object LinkingInfo { * implementationOptimizedForJavaScript() * }}} */ - @inline + @inline @linkTimeProperty("core/isWebAssembly") def isWebAssembly: Boolean = linkTimePropertyBoolean("core/isWebAssembly") /** Version of the linker. */ - @inline + @inline @linkTimeProperty("core/linkerVersion") def linkerVersion: String = linkTimePropertyString("core/linkerVersion") + /** Link-time conditional branching. + * + * A `linkTimeIf` expression behaves like an `if`, but it is guaranteed to + * be resolved at link-time. This prevents the unused branch to be linked at + * all. It can therefore reference APIs or language features that would + * otherwise fail to link. + * + * The condition `cond` can be constructed using: + * + * - Calls to methods annotated with `@linkTimeProperty` + * - Integer or boolean constants + * - Binary operators that return a boolean value + * + * A typical use case is to leverage the `**` operator on JavaScript + * `bigint`s if it is available, and otherwise fall back on using Scala + * `BigInt`s. Indeed, the `**` operator refuses to link when the target + * `esVersion` is too low. + * + * {{{ + * // Returns true iff 2^x < 10^y, for x and y positive integers + * def compareTwoPowTenPow(x: Int, y: Int): Boolean = { + * import scala.scalajs.LinkingInfo._ + * linkTimeIf(esVersion >= ESVersion.ES2020) { + * // JS bigints are available, and a fortiori their ** operator + * (js.BigInt(2) ** js.BigInt(x)) < (js.BigInt(10) ** js.BigInt(y)) + * } { + * // Fall back on Scala's BigInt's, which use a lot more code size + * BigInt(2).pow(x) < BigInt(10).pow(y) + * } + * } + * }}} + */ + def linkTimeIf[T](cond: Boolean)(thenp: T)(elsep: T): T = + throw new Error("stub") + /** Constants for the value of `esVersion`. */ object ESVersion { /** ECMAScrîpt 5.1. */ diff --git a/library/src/main/scala/scala/scalajs/annotation/linkTimeProperty.scala b/library/src/main/scala/scala/scalajs/annotation/linkTimeProperty.scala new file mode 100644 index 0000000000..6b93167c88 --- /dev/null +++ b/library/src/main/scala/scala/scalajs/annotation/linkTimeProperty.scala @@ -0,0 +1,33 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.scalajs.annotation + +/** Publicly marks the annotated method as being a link-time property. + * + * When an entity is annotated with `@linkTimeProperty`, its body must be a + * link-time property with the same `name`. The annotation makes that body + * "public", and it can therefore be inlined at call site at compile-time. + * + * From a user perspective, we can treat the presence of that annotation as if + * it were the `inline` keyword of Scala 3: it forces the inlining to happen + * at compile-time. + * + * This is necessary for the target method to be used in the condition of a + * `LinkingInfo.linkTimeIf`. + * + * @param name The name used to resolve the link-time value. + * + * @see [[LinkingInfo.linkTimeIf]] + */ +private[scalajs] final class linkTimeProperty(name: String) + extends scala.annotation.StaticAnnotation diff --git a/library/src/main/scala/scala/scalajs/runtime/package.scala b/library/src/main/scala/scala/scalajs/runtime/package.scala index d3ba4f766f..342081817d 100644 --- a/library/src/main/scala/scala/scalajs/runtime/package.scala +++ b/library/src/main/scala/scala/scalajs/runtime/package.scala @@ -111,7 +111,9 @@ package object runtime { } /** Identity hash code of an object. */ - def identityHashCode(x: Object): Int = throw new Error("stub") + @deprecated("Unused; use System.identityHashCode(x) instead.", since = "1.20.0") + def identityHashCode(x: Object): Int = + System.identityHashCode(x) def dynamicImport[A](thunk: DynamicImportThunk): js.Promise[A] = throw new Error("stub") diff --git a/linker-private-library/src/main/scala/org/scalajs/linker/runtime/FloatingPointBitsPolyfills.scala b/linker-private-library/src/main/scala/org/scalajs/linker/runtime/FloatingPointBitsPolyfills.scala new file mode 100644 index 0000000000..693b4f4136 --- /dev/null +++ b/linker-private-library/src/main/scala/org/scalajs/linker/runtime/FloatingPointBitsPolyfills.scala @@ -0,0 +1,190 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker.runtime + +import scala.scalajs.js + +/** Polyfills for manipulating the bits of floating point numbers without DataView. + * + * These polyfills are only used when targeting ECMAScript 5.1. + * + * Originally inspired by + * https://github.com/inexorabletash/polyfill/blob/3447582628b6e3ea81959c4d5987aa332c22d1ca/typedarray.js#L150-L264 + * + * Note that if typed arrays are not supported, it is almost certain that + * fround is not supported natively, so Float operations are extremely slow. + * + * We therefore do all computations in Doubles here. + */ +object FloatingPointBitsPolyfills { + private val floatPowsOf2: js.Array[Double] = + makePowsOf2(len = 1 << 8, java.lang.Float.MIN_NORMAL.toDouble) + + private val doublePowsOf2: js.Array[Double] = + makePowsOf2(len = 1 << 11, java.lang.Double.MIN_NORMAL) + + private def makePowsOf2(len: Int, minNormal: Double): js.Array[Double] = { + val r = new js.Array[Double](len) + r(0) = 0.0 + var i = 1 + var next = minNormal + while (i != len - 1) { + r(i) = next + i += 1 + next *= 2 + } + r(len - 1) = Double.PositiveInfinity + r + } + + @inline // inline into the static forwarder, which will be the entry point + def floatFromBits(bits: Int): Double = { + val ebits = 8 + val fbits = 23 + val sign = (bits >> 31) | 1 // -1 or 1 + val e = (bits >> fbits) & ((1 << ebits) - 1) + val f = bits & ((1 << fbits) - 1) + decodeIEEE754(ebits, fbits, floatPowsOf2, Float.MinPositiveValue, sign, e, f) + } + + @inline // inline into the static forwarder, which will be the entry point + def floatToBits(floatValue: Float): Int = { + // Some constants + val ebits = 8 + val fbits = 23 + + // Force computations to be on Doubles + val value = floatValue.toDouble + + // Determine sign bit and compute the absolute value av + val sign = if (value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)) -1 else 1 + val s = sign & Int.MinValue + val av = sign * value + + // Compute e and f + val powsOf2 = this.floatPowsOf2 // local cache + val e = encodeIEEE754Exponent(ebits, powsOf2, av) + val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, Float.MinPositiveValue.toDouble, av, e) + + // Encode + s | (e << fbits) | rawToInt(f) + } + + @inline // inline into the static forwarder, which will be the entry point + def doubleFromBits(bits: Long): Double = { + val ebits = 11 + val fbits = 52 + val hifbits = fbits - 32 + val hi = (bits >>> 32).toInt + val lo = toUint(bits.toInt) + val sign = (hi >> 31) | 1 // -1 or 1 + val e = (hi >> hifbits) & ((1 << ebits) - 1) + val f = (hi & ((1 << hifbits) - 1)).toDouble * 0x100000000L.toDouble + lo + decodeIEEE754(ebits, fbits, doublePowsOf2, Double.MinPositiveValue, sign, e, f) + } + + @inline // inline into the static forwarder, which will be the entry point + def doubleToBits(value: Double): Long = { + // Some constants + val ebits = 11 + val fbits = 52 + val hifbits = fbits - 32 + + // Determine sign bit and compute the absolute value av + val sign = if (value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)) -1 else 1 + val s = sign & Int.MinValue + val av = sign * value + + // Compute e and f + val powsOf2 = this.doublePowsOf2 // local cache + val e = encodeIEEE754Exponent(ebits, powsOf2, av) + val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, Double.MinPositiveValue, av, e) + + // Encode + val hi = s | (e << hifbits) | rawToInt(f / 0x100000000L.toDouble) + val lo = rawToInt(f) + (hi.toLong << 32) | (lo.toLong & 0xffffffffL) + } + + @inline + private def decodeIEEE754(ebits: Int, fbits: Int, + powsOf2: js.Array[Double], minPositiveValue: Double, + sign: Int, e: Int, f: Double): Double = { + + // Some constants + val specialExponent = (1 << ebits) - 1 + val twoPowFbits = (1L << fbits).toDouble + + if (e == specialExponent) { + // Special + if (f == 0.0) + sign * Double.PositiveInfinity + else + Double.NaN + } else if (e > 0) { + // Normalized + sign * powsOf2(e) * (1 + f / twoPowFbits) + } else { + // Subnormal + sign * f * minPositiveValue + } + } + + @inline + private def encodeIEEE754Exponent(ebits: Int, + powsOf2: js.Array[Double], av: Double): Int = { + + /* Binary search of `av` inside `powsOf2`. + * There are exactly `ebits` iterations of this loop (11 for Double, 8 for Float). + */ + var eMin = 0 + var eMax = 1 << ebits + while (eMin + 1 < eMax) { + val e = (eMin + eMax) >> 1 + if (av < powsOf2(e)) // false when av is NaN + eMax = e + else + eMin = e + } + eMin + } + + @inline + private def encodeIEEE754MantissaBits(ebits: Int, fbits: Int, + powsOf2: js.Array[Double], minPositiveValue: Double, + av: Double, e: Int): Double = { + + // Some constants + val specialExponent = (1 << ebits) - 1 + val twoPowFbits = (1L << fbits).toDouble + + if (e == specialExponent) { + if (av != av) + (1L << (fbits - 1)).toDouble // NaN + else + 0.0 // Infinity + } else { + if (e == 0) + av / minPositiveValue // Subnormal + else + ((av / powsOf2(e)) - 1.0) * twoPowFbits // Normal + } + } + + @inline private def toUint(x: Int): Double = + (x.asInstanceOf[js.Dynamic] >>> 0.asInstanceOf[js.Dynamic]).asInstanceOf[Double] + + @inline private def rawToInt(x: Double): Int = + (x.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int] + +} diff --git a/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala b/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala index 7a90e2a9e1..045ac6bf9d 100644 --- a/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala +++ b/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala @@ -40,74 +40,77 @@ import scala.annotation.tailrec /** Emulates a Long on the JavaScript platform. */ @inline final class RuntimeLong(val lo: Int, val hi: Int) { - a => - import RuntimeLong._ - // Universal equality + // java.lang.Object @inline override def equals(that: Any): Boolean = that match { - case b: RuntimeLong => inline_equals(b) - case _ => false + case that: RuntimeLong => RuntimeLong.equals(this, that) + case _ => false } @inline override def hashCode(): Int = lo ^ hi - // String operations - @inline override def toString(): String = - RuntimeLong.toString(lo, hi) - - // Conversions - - @inline def toByte: Byte = lo.toByte - @inline def toShort: Short = lo.toShort - @inline def toChar: Char = lo.toChar - @inline def toInt: Int = lo - @inline def toLong: Long = this.asInstanceOf[Long] - @inline def toFloat: Float = RuntimeLong.toFloat(lo, hi) - @inline def toDouble: Double = RuntimeLong.toDouble(lo, hi) + RuntimeLong.toString(this) // java.lang.Number - @inline def byteValue(): Byte = toByte - @inline def shortValue(): Short = toShort - @inline def intValue(): Int = toInt - @inline def longValue(): Long = toLong - @inline def floatValue(): Float = toFloat - @inline def doubleValue(): Double = toDouble + @inline def byteValue(): Byte = lo.toByte + @inline def shortValue(): Short = lo.toShort + @inline def intValue(): Int = lo + @inline def longValue(): Long = this.asInstanceOf[Long] + @inline def floatValue(): Float = RuntimeLong.toFloat(this) + @inline def doubleValue(): Double = RuntimeLong.toDouble(this) // java.lang.Comparable, including bridges @inline def compareTo(that: Object): Int = - compareTo(that.asInstanceOf[RuntimeLong]) + RuntimeLong.compare(this, that.asInstanceOf[RuntimeLong]) @inline def compareTo(that: java.lang.Long): Int = - compareTo(that.asInstanceOf[RuntimeLong]) + RuntimeLong.compare(this, that.asInstanceOf[RuntimeLong]) + + // A few operator-friendly methods used by the division algorithms + + @inline private def <<(b: Int): RuntimeLong = RuntimeLong.shl(this, b) + @inline private def >>>(b: Int): RuntimeLong = RuntimeLong.shr(this, b) + @inline private def +(b: RuntimeLong): RuntimeLong = RuntimeLong.add(this, b) + @inline private def -(b: RuntimeLong): RuntimeLong = RuntimeLong.sub(this, b) +} + +object RuntimeLong { + private final val TwoPow32 = 4294967296.0 + private final val TwoPow63 = 9223372036854775808.0 + + /** The magical mask that allows to test whether an unsigned long is a safe + * double. + * @see isUnsignedSafeDouble + */ + private final val UnsignedSafeDoubleHiMask = 0xffe00000 + + /** The hi part of a (lo, hi) return value. */ + private[this] var hiReturn: Int = _ // Comparisons @inline - def compareTo(b: RuntimeLong): Int = + def compare(a: RuntimeLong, b: RuntimeLong): Int = RuntimeLong.compare(a.lo, a.hi, b.lo, b.hi) @inline - private def inline_equals(b: RuntimeLong): Boolean = + def equals(a: RuntimeLong, b: RuntimeLong): Boolean = a.lo == b.lo && a.hi == b.hi @inline - def equals(b: RuntimeLong): Boolean = - inline_equals(b) + def notEquals(a: RuntimeLong, b: RuntimeLong): Boolean = + !equals(a, b) @inline - def notEquals(b: RuntimeLong): Boolean = - !inline_equals(b) - - @inline - def <(b: RuntimeLong): Boolean = { + def lt(a: RuntimeLong, b: RuntimeLong): Boolean = { /* We should use `inlineUnsignedInt_<(a.lo, b.lo)`, but that first extracts * a.lo and b.lo into local variables, which cause the if/else not to be * a valid JavaScript expression anymore. This causes useless explosion of @@ -121,7 +124,7 @@ final class RuntimeLong(val lo: Int, val hi: Int) { } @inline - def <=(b: RuntimeLong): Boolean = { + def le(a: RuntimeLong, b: RuntimeLong): Boolean = { /* Manually inline `inlineUnsignedInt_<=(a.lo, b.lo)`. * See the comment in `<` for the rationale. */ @@ -132,7 +135,7 @@ final class RuntimeLong(val lo: Int, val hi: Int) { } @inline - def >(b: RuntimeLong): Boolean = { + def gt(a: RuntimeLong, b: RuntimeLong): Boolean = { /* Manually inline `inlineUnsignedInt_>a.lo, b.lo)`. * See the comment in `<` for the rationale. */ @@ -143,7 +146,7 @@ final class RuntimeLong(val lo: Int, val hi: Int) { } @inline - def >=(b: RuntimeLong): Boolean = { + def ge(a: RuntimeLong, b: RuntimeLong): Boolean = { /* Manually inline `inlineUnsignedInt_>=(a.lo, b.lo)`. * See the comment in `<` for the rationale. */ @@ -153,29 +156,69 @@ final class RuntimeLong(val lo: Int, val hi: Int) { else ahi > bhi } - // Bitwise operations + @inline + def ltu(a: RuntimeLong, b: RuntimeLong): Boolean = { + /* Manually inline `inlineUnsignedInt_<(a.lo, b.lo)`. + * See the comment in `<` for the rationale. + */ + val ahi = a.hi + val bhi = b.hi + if (ahi == bhi) (a.lo ^ 0x80000000) < (b.lo ^ 0x80000000) + else inlineUnsignedInt_<(ahi, bhi) + } @inline - def unary_~ : RuntimeLong = // scalastyle:ignore - new RuntimeLong(~lo, ~hi) + def leu(a: RuntimeLong, b: RuntimeLong): Boolean = { + /* Manually inline `inlineUnsignedInt_<=(a.lo, b.lo)`. + * See the comment in `<` for the rationale. + */ + val ahi = a.hi + val bhi = b.hi + if (ahi == bhi) (a.lo ^ 0x80000000) <= (b.lo ^ 0x80000000) + else inlineUnsignedInt_<=(ahi, bhi) + } @inline - def |(b: RuntimeLong): RuntimeLong = + def gtu(a: RuntimeLong, b: RuntimeLong): Boolean = { + /* Manually inline `inlineUnsignedInt_>(a.lo, b.lo)`. + * See the comment in `<` for the rationale. + */ + val ahi = a.hi + val bhi = b.hi + if (ahi == bhi) (a.lo ^ 0x80000000) > (b.lo ^ 0x80000000) + else inlineUnsignedInt_>(ahi, bhi) + } + + @inline + def geu(a: RuntimeLong, b: RuntimeLong): Boolean = { + /* Manually inline `inlineUnsignedInt_>=(a.lo, b.lo)`. + * See the comment in `<` for the rationale. + */ + val ahi = a.hi + val bhi = b.hi + if (ahi == bhi) (a.lo ^ 0x80000000) >= (b.lo ^ 0x80000000) + else inlineUnsignedInt_>=(ahi, bhi) + } + + // Bitwise operations + + @inline + def or(a: RuntimeLong, b: RuntimeLong): RuntimeLong = new RuntimeLong(a.lo | b.lo, a.hi | b.hi) @inline - def &(b: RuntimeLong): RuntimeLong = + def and(a: RuntimeLong, b: RuntimeLong): RuntimeLong = new RuntimeLong(a.lo & b.lo, a.hi & b.hi) @inline - def ^(b: RuntimeLong): RuntimeLong = + def xor(a: RuntimeLong, b: RuntimeLong): RuntimeLong = new RuntimeLong(a.lo ^ b.lo, a.hi ^ b.hi) // Shifts /** Shift left */ @inline - def <<(n: Int): RuntimeLong = { + def shl(a: RuntimeLong, n: Int): RuntimeLong = { /* This should *reasonably* be: * val n1 = n & 63 * if (n1 < 32) @@ -241,63 +284,63 @@ final class RuntimeLong(val lo: Int, val hi: Int) { * * Finally we have: */ - val lo = this.lo + val lo = a.lo new RuntimeLong( if ((n & 32) == 0) lo << n else 0, - if ((n & 32) == 0) (lo >>> 1 >>> (31-n)) | (hi << n) else lo << n) + if ((n & 32) == 0) (lo >>> 1 >>> (31-n)) | (a.hi << n) else lo << n) } /** Logical shift right */ @inline - def >>>(n: Int): RuntimeLong = { + def shr(a: RuntimeLong, n: Int): RuntimeLong = { // This derives in a similar way as in << - val hi = this.hi + val hi = a.hi new RuntimeLong( - if ((n & 32) == 0) (lo >>> n) | (hi << 1 << (31-n)) else hi >>> n, + if ((n & 32) == 0) (a.lo >>> n) | (hi << 1 << (31-n)) else hi >>> n, if ((n & 32) == 0) hi >>> n else 0) } /** Arithmetic shift right */ @inline - def >>(n: Int): RuntimeLong = { + def sar(a: RuntimeLong, n: Int): RuntimeLong = { // This derives in a similar way as in << - val hi = this.hi + val hi = a.hi new RuntimeLong( - if ((n & 32) == 0) (lo >>> n) | (hi << 1 << (31-n)) else hi >> n, + if ((n & 32) == 0) (a.lo >>> n) | (hi << 1 << (31-n)) else hi >> n, if ((n & 32) == 0) hi >> n else hi >> 31) } // Arithmetic operations @inline - def unary_- : RuntimeLong = { // scalastyle:ignore - val lo = this.lo - val hi = this.hi - new RuntimeLong(inline_lo_unary_-(lo), inline_hi_unary_-(lo, hi)) - } - - @inline - def +(b: RuntimeLong): RuntimeLong = { + def add(a: RuntimeLong, b: RuntimeLong): RuntimeLong = { + // Hacker's Delight, Section 2-16 val alo = a.lo - val ahi = a.hi - val bhi = b.hi - val lo = alo + b.lo + val blo = b.lo + val lo = alo + blo new RuntimeLong(lo, - if (inlineUnsignedInt_<(lo, alo)) ahi + bhi + 1 else ahi + bhi) + a.hi + b.hi + (((alo & blo) | ((alo | blo) & ~lo)) >>> 31)) } @inline - def -(b: RuntimeLong): RuntimeLong = { + def sub(a: RuntimeLong, b: RuntimeLong): RuntimeLong = { + /* Hacker's Delight, Section 2-16 + * + * We deviate a bit from the original algorithm. Hacker's Delight uses + * `- (... >>> 31)`. Instead, we use `+ (... >> 31)`. These are equivalent, + * since `(x >> 31) == -(x >>> 31)` for all x. The variant with `+` folds + * better when `a.hi` and `b.hi` are both known to be 0. This happens in + * practice when `a` and `b` are 0-extended from `Int` values. + */ val alo = a.lo - val ahi = a.hi - val bhi = b.hi - val lo = alo - b.lo + val blo = b.lo + val lo = alo - blo new RuntimeLong(lo, - if (inlineUnsignedInt_>(lo, alo)) ahi - bhi - 1 else ahi - bhi) + a.hi - b.hi + (((~alo & blo) | (~(alo ^ blo) & lo)) >> 31)) } @inline - def *(b: RuntimeLong): RuntimeLong = { + def mul(a: RuntimeLong, b: RuntimeLong): RuntimeLong = { /* The following algorithm is based on the decomposition in 32-bit and then * 16-bit subproducts of the unsigned interpretation of operands. * @@ -523,48 +566,30 @@ final class RuntimeLong(val lo: Int, val hi: Int) { new RuntimeLong(lo, hi) } + /** Computes `longBitsToDouble(a)`. + * + * `fpBitsDataView` must be a scratch `js.typedarray.DataView` whose + * underlying buffer is at least 8 bytes long. + */ @inline - def /(b: RuntimeLong): RuntimeLong = - RuntimeLong.divide(a, b) - - /** `java.lang.Long.divideUnsigned(a, b)` */ - @inline - def divideUnsigned(b: RuntimeLong): RuntimeLong = - RuntimeLong.divideUnsigned(a, b) + def bitsToDouble(a: RuntimeLong, + fpBitsDataView: scala.scalajs.js.typedarray.DataView): Double = { - @inline - def %(b: RuntimeLong): RuntimeLong = - RuntimeLong.remainder(a, b) + fpBitsDataView.setInt32(0, a.lo, littleEndian = true) + fpBitsDataView.setInt32(4, a.hi, littleEndian = true) + fpBitsDataView.getFloat64(0, littleEndian = true) + } - /** `java.lang.Long.remainderUnsigned(a, b)` */ @inline - def remainderUnsigned(b: RuntimeLong): RuntimeLong = - RuntimeLong.remainderUnsigned(a, b) - -} - -object RuntimeLong { - private final val TwoPow32 = 4294967296.0 - private final val TwoPow63 = 9223372036854775808.0 - - /** The magical mask that allows to test whether an unsigned long is a safe - * double. - * @see isUnsignedSafeDouble - */ - private final val UnsignedSafeDoubleHiMask = 0xffe00000 - - private final val AskQuotient = 0 - private final val AskRemainder = 1 - private final val AskToString = 2 - - /** The hi part of a (lo, hi) return value. */ - private[this] var hiReturn: Int = _ + def toString(a: RuntimeLong): String = + toString(a.lo, a.hi) private def toString(lo: Int, hi: Int): String = { if (isInt32(lo, hi)) { lo.toString() } else if (hi < 0) { - "-" + toUnsignedString(inline_lo_unary_-(lo), inline_hi_unary_-(lo, hi)) + val neg = inline_negate(lo, hi) + "-" + toUnsignedString(neg.lo, neg.hi) } else { toUnsignedString(lo, hi) } @@ -578,7 +603,8 @@ object RuntimeLong { asUnsignedSafeDouble(lo, hi).toString } else { /* At this point, (lo, hi) >= 2^53. - * We divide (lo, hi) once by 10^9 and keep the remainder. + * + * The idea is to divide (lo, hi) once by 10^9 and keep the remainder. * * The remainder must then be < 10^9, and is therefore an int32. * @@ -586,26 +612,102 @@ object RuntimeLong { * is therefore a valid double. It must also be non-zero, since * (lo, hi) >= 2^53 > 10^9. * - * To avoid allocating a tuple with the quotient and remainder, we push - * the final conversion to string inside unsignedDivModHelper. According - * to micro-benchmarks, this optimization makes toString 25% faster in - * this branch. + * We should do that single division as a Long division. However, that is + * slow. We can cheat with a Double division instead. + * + * We convert the unsigned value num = (lo, hi) to a Double value + * approxNum. This is an approximation. It can lose as many as + * 64 - 53 = 11 low-order bits. Hence |approxNum - num| <= 2^12. + * + * We then compute an approximated quotient + * approxQuot = floor(approxNum / 10^9) + * instead of the theoretical value + * quot = floor(num / 10^9) + * + * Since 10^9 > 2^29 > 2^12, we have |approxNum - num| < 10^9. + * Therefore, |approxQuot - quot| <= 1. + * + * We also have 0 <= approxQuot < 2^53, which means that approxQuot is an + * "unsigned safe double" and that `approxQuot.toLong` is lossless. + * + * At this point, we compute the approximated remainder + * approxRem = num - 10^9 * approxQuot.toLong + * as if with Long arithmetics. + * + * Since the theoretical remainder rem = num - 10^9 * quot is such that + * 0 <= rem < 10^9, and since |approxQuot - quot| <= 1, we have that + * -10^9 <= approxRem < 2 * 10^9 + * + * Interestingly, that range entirely fits within a signed int32. + * That means approxRem = approxRem.toInt, and therefore + * + * approxRem + * = (num - 10^9 * approxQuot.toLong).toInt + * = num.toInt - 10^9 * approxQuot.toLong.toInt (thanks to modular arithmetics) + * = lo - 10^9 * unsignedSafeDoubleLo(approxQuot) + * + * That allows to compute approxRem with Int arithmetics without loss of + * precision. + * + * We can use approxRem to detect and correct the error on approxQuot. + * If approxRem < 0, correct approxQuot by -1 and approxRem by +10^9. + * If approxRem >= 10^9, correct them by +1 and -10^9, respectively. + * + * After the correction, we know that approxQuot and approxRem are equal + * to their theoretical counterparts quot and rem. We have successfully + * computed the correct quotient and remainder without using any Long + * division. + * + * We can finally convert both to strings using the native string + * conversions, and concatenate the results to produce our final result. */ - unsignedDivModHelper(lo, hi, 1000000000, 0, - AskToString).asInstanceOf[String] + + // constants + val divisor = 1000000000 // 10^9 + val divisorInv = 1.0 / divisor.toDouble + + // initial approximation of the quotient and remainder + val approxNum = asUint(hi) * TwoPow32 + asUint(lo) + var approxQuot = scala.scalajs.js.Math.floor(approxNum * divisorInv) + var approxRem = lo - divisor * unsignedSafeDoubleLo(approxQuot) + + // correct the approximations + if (approxRem < 0) { + approxQuot -= 1.0 + approxRem += divisor + } else if (approxRem >= divisor) { + approxQuot += 1.0 + approxRem -= divisor + } + + // build the result string + val remStr = approxRem.toString() + approxQuot.toString() + substring("000000000", remStr.length()) + remStr } } + @inline + def toInt(a: RuntimeLong): Int = + a.lo + + @inline + def toDouble(a: RuntimeLong): Double = + toDouble(a.lo, a.hi) + private def toDouble(lo: Int, hi: Int): Double = { if (hi < 0) { // We do asUint() on the hi part specifically for MinValue - -(asUint(inline_hi_unary_-(lo, hi)) * TwoPow32 + - asUint(inline_lo_unary_-(lo))) + val neg = inline_negate(lo, hi) + -(asUint(neg.hi) * TwoPow32 + asUint(neg.lo)) } else { hi * TwoPow32 + asUint(lo) } } + @inline + def toFloat(a: RuntimeLong): Float = + toFloat(a.lo, a.hi) + private def toFloat(lo: Int, hi: Int): Float = { /* This implementation is based on the property that, *if* the conversion * `x.toDouble` is lossless, then the result of `x.toFloat` is equivalent @@ -661,10 +763,21 @@ object RuntimeLong { (if (hi < 0) -absRes else absRes).toFloat } + @inline + def clz(a: RuntimeLong): Int = { + val hi = a.hi + if (hi != 0) Integer.numberOfLeadingZeros(hi) + else 32 + Integer.numberOfLeadingZeros(a.lo) + } + @inline def fromInt(value: Int): RuntimeLong = new RuntimeLong(value, value >> 31) + @inline + def fromUnsignedInt(value: Int): RuntimeLong = + new RuntimeLong(value, 0) + @inline def fromDouble(value: Double): RuntimeLong = { val lo = fromDoubleImpl(value) @@ -728,6 +841,22 @@ object RuntimeLong { } } + /** Computes `doubleToLongBits(value)`. + * + * `fpBitsDataView` must be a scratch `js.typedarray.DataView` whose + * underlying buffer is at least 8 bytes long. + */ + @inline + def fromDoubleBits(value: Double, + fpBitsDataView: scala.scalajs.js.typedarray.DataView): RuntimeLong = { + + fpBitsDataView.setFloat64(0, value, littleEndian = true) + new RuntimeLong( + fpBitsDataView.getInt32(0, littleEndian = true), + fpBitsDataView.getInt32(4, littleEndian = true) + ) + } + private def compare(alo: Int, ahi: Int, blo: Int, bhi: Int): Int = { if (ahi == bhi) { if (alo == blo) 0 @@ -739,6 +868,47 @@ object RuntimeLong { } } + /** Intrinsic for Math.multiplyFull. + * + * Compared to the regular expansion of `x.toLong * y.toLong`, this + * intrinsic avoids 2 int multiplications. + */ + @inline + def multiplyFull(a: Int, b: Int): RuntimeLong = { + /* We use Hacker's Delight, Section 8-2, Figure 8-2, to compute the hi + * word of the result. We reuse intermediate products to compute the lo + * word, like we do in `RuntimeLong.*`. + * + * We swap the role of a1b0 and a0b1 compared to Hacker's Delight, to + * optimize for the case where a1b0 collapses to 0, like we do in + * `RuntimeLong.*`. The optimizer normalizes constants in multiplyFull to + * be on the left-hand-side (when it cannot do constant-folding to begin + * with). Therefore, `b` is never constant in practice. + */ + + val a0 = a & 0xffff + val a1 = a >> 16 + val b0 = b & 0xffff + val b1 = b >> 16 + + val a0b0 = a0 * b0 + val a1b0 = a1 * b0 // collapses to 0 when a is constant and 0 <= a <= 0xffff + val a0b1 = a0 * b1 + + /* lo = a * b, but we compute the above 3 subproducts for hi anyway, + * so we reuse them to compute lo too, trading a * for 2 +'s and 1 <<. + */ + val lo = a0b0 + ((a1b0 + a0b1) << 16) + + val t = a0b1 + (a0b0 >>> 16) + val hi = { + a1 * b1 + (t >> 16) + + (((t & 0xffff) + a1b0) >> 16) // collapses to 0 when a1b0 = 0 + } + + new RuntimeLong(lo, hi) + } + @inline def divide(a: RuntimeLong, b: RuntimeLong): RuntimeLong = { val lo = divideImpl(a.lo, a.hi, b.lo, b.hi) @@ -775,7 +945,7 @@ object RuntimeLong { val bAbs = inline_abs(blo, bhi) val absRLo = unsigned_/(aAbs.lo, aAbs.hi, bAbs.lo, bAbs.hi) if ((ahi ^ bhi) >= 0) absRLo // a and b have the same sign bit - else inline_hiReturn_unary_-(absRLo, hiReturn) + else inline_negate_hiReturn(absRLo, hiReturn) } } @@ -827,7 +997,7 @@ object RuntimeLong { hiReturn = 0 ahi >>> pow } else { - unsignedDivModHelper(alo, ahi, blo, bhi, AskQuotient).asInstanceOf[Int] + unsignedDivModHelper(alo, ahi, blo, bhi, askQuotient = true) } } } @@ -868,7 +1038,7 @@ object RuntimeLong { val aAbs = inline_abs(alo, ahi) val bAbs = inline_abs(blo, bhi) val absRLo = unsigned_%(aAbs.lo, aAbs.hi, bAbs.lo, bAbs.hi) - if (ahi < 0) inline_hiReturn_unary_-(absRLo, hiReturn) + if (ahi < 0) inline_negate_hiReturn(absRLo, hiReturn) else absRLo } } @@ -919,22 +1089,19 @@ object RuntimeLong { hiReturn = ahi & (bhi - 1) alo } else { - unsignedDivModHelper(alo, ahi, blo, bhi, AskRemainder).asInstanceOf[Int] + unsignedDivModHelper(alo, ahi, blo, bhi, askQuotient = false) } } } - /** Helper for `unsigned_/`, `unsigned_%` and `toUnsignedString()`. + /** Helper for `unsigned_/` and `unsigned_%`. * - * The value of `ask` may be one of: - * - * - `AskQuotient`: returns the quotient (with the hi part in `hiReturn`) - * - `AskRemainder`: returns the remainder (with the hi part in `hiReturn`) - * - `AskToString`: returns the conversion of `(alo, ahi)` to string. - * In this case, `blo` must be 10^9 and `bhi` must be 0. + * If `askQuotient` is true, computes the quotient, otherwise computes the + * remainder. Stores the hi word of the result in `hiReturn`, and returns + * the lo word. */ private def unsignedDivModHelper(alo: Int, ahi: Int, blo: Int, bhi: Int, - ask: Int): Any = { + askQuotient: Boolean): Int = { var shift = inlineNumberOfLeadingZeros(blo, bhi) - inlineNumberOfLeadingZeros(alo, ahi) @@ -981,40 +1148,27 @@ object RuntimeLong { val remDouble = asUnsignedSafeDouble(remLo, remHi) val bDouble = asUnsignedSafeDouble(blo, bhi) - if (ask != AskRemainder) { + if (askQuotient) { val rem_div_bDouble = fromUnsignedSafeDouble(remDouble / bDouble) val newQuot = new RuntimeLong(quotLo, quotHi) + rem_div_bDouble - quotLo = newQuot.lo - quotHi = newQuot.hi - } - - if (ask != AskQuotient) { + hiReturn = newQuot.hi + newQuot.lo + } else { val rem_mod_bDouble = remDouble % bDouble - remLo = unsignedSafeDoubleLo(rem_mod_bDouble) - remHi = unsignedSafeDoubleHi(rem_mod_bDouble) + hiReturn = unsignedSafeDoubleHi(rem_mod_bDouble) + unsignedSafeDoubleLo(rem_mod_bDouble) } - } - - if (ask == AskQuotient) { - hiReturn = quotHi - quotLo - } else if (ask == AskRemainder) { - hiReturn = remHi - remLo } else { - // AskToString (recall that b = 10^9 in this case) - val quot = asUnsignedSafeDouble(quotLo, quotHi) // != 0 - val remStr = remLo.toString // remHi is always 0 - quot.toString + substring("000000000", remStr.length) + remStr + if (askQuotient) { + hiReturn = quotHi + quotLo + } else { + hiReturn = remHi + remLo + } } } - @inline - private def inline_hiReturn_unary_-(lo: Int, hi: Int): Int = { - hiReturn = inline_hi_unary_-(lo, hi) - inline_lo_unary_-(lo) - } - @inline private def substring(s: String, start: Int): String = { import scala.scalajs.js.JSStringOps.enableJSStringOps @@ -1098,6 +1252,10 @@ object RuntimeLong { def inlineUnsignedInt_<(a: Int, b: Int): Boolean = (a ^ 0x80000000) < (b ^ 0x80000000) + @inline + def inlineUnsignedInt_<=(a: Int, b: Int): Boolean = + (a ^ 0x80000000) <= (b ^ 0x80000000) + @inline def inlineUnsignedInt_>(a: Int, b: Int): Boolean = (a ^ 0x80000000) > (b ^ 0x80000000) @@ -1107,17 +1265,20 @@ object RuntimeLong { (a ^ 0x80000000) >= (b ^ 0x80000000) @inline - def inline_lo_unary_-(lo: Int): Int = - -lo + def inline_negate(lo: Int, hi: Int): RuntimeLong = + sub(new RuntimeLong(0, 0), new RuntimeLong(lo, hi)) @inline - def inline_hi_unary_-(lo: Int, hi: Int): Int = - if (lo != 0) ~hi else -hi + def inline_negate_hiReturn(lo: Int, hi: Int): Int = { + val n = inline_negate(lo, hi) + hiReturn = n.hi + n.lo + } @inline def inline_abs(lo: Int, hi: Int): RuntimeLong = { if (hi < 0) - new RuntimeLong(inline_lo_unary_-(lo), inline_hi_unary_-(lo, hi)) + inline_negate(lo, hi) else new RuntimeLong(lo, hi) } diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala index 2397ff94af..56c0232121 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala @@ -376,6 +376,8 @@ private class ClosureAstTransformer(featureSet: FeatureSet, if (value) new Node(Token.TRUE) else new Node(Token.FALSE) case IntLiteral(value) => mkNumberLiteral(value) + case UintLiteral(value) => + mkNumberLiteral(Integer.toUnsignedLong(value).toDouble) case DoubleLiteral(value) => mkNumberLiteral(value) case StringLiteral(value) => diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala index d668c26e25..c4849c8955 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala @@ -24,6 +24,8 @@ object PrivateLibHolder { private val stableVersion = ir.Version.fromInt(0) // never changes private val sjsirPaths = Seq( + "org/scalajs/linker/runtime/FloatingPointBitsPolyfills.sjsir", + "org/scalajs/linker/runtime/FloatingPointBitsPolyfills$.sjsir", "org/scalajs/linker/runtime/RuntimeLong.sjsir", "org/scalajs/linker/runtime/RuntimeLong$.sjsir", "org/scalajs/linker/runtime/UndefinedBehaviorError.sjsir", diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala index 9a80ac96a2..c3b428dbeb 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala @@ -31,7 +31,7 @@ import org.scalajs.ir.WellKnownNames._ import org.scalajs.linker._ import org.scalajs.linker.checker.CheckingPhase -import org.scalajs.linker.frontend.{IRLoader, LambdaSynthesizer, SyntheticClassKind} +import org.scalajs.linker.frontend.{IRLoader, LambdaSynthesizer, LinkTimeProperties, SyntheticClassKind} import org.scalajs.linker.interface._ import org.scalajs.linker.interface.unstable.ModuleInitializerImpl import org.scalajs.linker.standard._ @@ -47,15 +47,17 @@ import Infos.{NamespacedMethodName, ReachabilityInfo, ReachabilityInfoInClass} final class Analyzer(config: CommonPhaseConfig, initial: Boolean, checkIRFor: Option[CheckingPhase], failOnError: Boolean, irLoader: IRLoader) { + private val linkTimeProperties = LinkTimeProperties.fromCoreSpec(config.coreSpec) + private val infoLoader: InfoLoader = - new InfoLoader(irLoader, checkIRFor) + new InfoLoader(irLoader, checkIRFor, linkTimeProperties) def computeReachability(moduleInitializers: Seq[ModuleInitializer], symbolRequirements: SymbolRequirement, logger: Logger)(implicit ec: ExecutionContext): Future[Analysis] = { infoLoader.update(logger) - val run = new AnalyzerRun(config, initial, infoLoader)( + val run = new AnalyzerRun(config, initial, infoLoader, linkTimeProperties)( adjustExecutionContextForParallelism(ec, config.parallel)) run @@ -99,7 +101,10 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean, } private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, - infoLoader: InfoLoader)(implicit ec: ExecutionContext) extends Analysis { + infoLoader: InfoLoader, linkTimeProperties: LinkTimeProperties)( + implicit ec: ExecutionContext) + extends Analysis { + import AnalyzerRun._ private val allowAddingSyntheticMethods = initial @@ -1488,6 +1493,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, lookupOrSynthesizeClass(className, SyntheticClassKind.Lambda(descriptor)) { lambdaClassInfo => lambdaClassInfo.instantiated() lambdaClassInfo.callMethodStatically(MemberNamespace.Constructor, ctorName) + moduleUnit.addStaticDependency(lambdaClassInfo.className) } } } @@ -1538,7 +1544,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, if (data.referencedLinkTimeProperties.nonEmpty) { for ((name, tpe) <- data.referencedLinkTimeProperties) { - if (!config.coreSpec.linkTimeProperties.validate(name, tpe)) { + if (!linkTimeProperties.get(name).exists(_.tpe == tpe)) { _errors ::= InvalidLinkTimeProperty(name, tpe, from) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala index 83003e6be5..c791727110 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala @@ -23,13 +23,16 @@ import org.scalajs.ir.Trees._ import org.scalajs.logging._ import org.scalajs.linker.checker._ -import org.scalajs.linker.frontend.IRLoader +import org.scalajs.linker.frontend.{IRLoader, LinkTimeProperties} import org.scalajs.linker.interface.LinkingException import org.scalajs.linker.CollectionsCompat.MutableMapCompatOps import Platform.emptyThreadSafeMap -private[analyzer] final class InfoLoader(irLoader: IRLoader, checkIRFor: Option[CheckingPhase]) { +private[analyzer] final class InfoLoader(irLoader: IRLoader, + checkIRFor: Option[CheckingPhase], linkTimeProperties: LinkTimeProperties) { + + private val generator = new Infos.InfoGenerator(linkTimeProperties) private var logger: Logger = _ private val cache = emptyThreadSafeMap[ClassName, InfoLoader.ClassInfoCache] @@ -44,7 +47,7 @@ private[analyzer] final class InfoLoader(irLoader: IRLoader, checkIRFor: Option[ implicit ec: ExecutionContext): Option[Future[Infos.ClassInfo]] = { if (irLoader.classExists(className)) { val infoCache = cache.getOrElseUpdate(className, - new InfoLoader.ClassInfoCache(className, irLoader, checkIRFor)) + new InfoLoader.ClassInfoCache(className, irLoader, checkIRFor, generator)) Some(infoCache.loadInfo(logger)) } else { None @@ -60,7 +63,9 @@ private[analyzer] final class InfoLoader(irLoader: IRLoader, checkIRFor: Option[ private[analyzer] object InfoLoader { private type MethodInfos = Array[Map[MethodName, Infos.MethodInfo]] - private class ClassInfoCache(className: ClassName, irLoader: IRLoader, checkIRFor: Option[CheckingPhase]) { + private class ClassInfoCache(className: ClassName, irLoader: IRLoader, + checkIRFor: Option[CheckingPhase], generator: Infos.InfoGenerator) { + private var cacheUsed: Boolean = false private var version: Version = Version.Unversioned private var info: Future[Infos.ClassInfo] = _ @@ -103,12 +108,12 @@ private[analyzer] object InfoLoader { } private def generateInfos(classDef: ClassDef): Infos.ClassInfo = { - val referencedFieldClasses = Infos.genReferencedFieldClasses(classDef.fields) + val referencedFieldClasses = generator.genReferencedFieldClasses(classDef.fields) - prevMethodInfos = genMethodInfos(classDef.methods, prevMethodInfos) - prevJSCtorInfo = genJSCtorInfo(classDef.jsConstructor, prevJSCtorInfo) + prevMethodInfos = genMethodInfos(classDef.methods, prevMethodInfos, generator) + prevJSCtorInfo = genJSCtorInfo(classDef.jsConstructor, prevJSCtorInfo, generator) prevJSMethodPropDefInfos = - genJSMethodPropDefInfos(classDef.jsMethodProps, prevJSMethodPropDefInfos) + genJSMethodPropDefInfos(classDef.jsMethodProps, prevJSMethodPropDefInfos, generator) val exportedMembers = prevJSCtorInfo.toList ::: prevJSMethodPropDefInfos @@ -116,7 +121,7 @@ private[analyzer] object InfoLoader { * and usually quite small when they exist. */ val topLevelExports = classDef.topLevelExportDefs - .map(Infos.generateTopLevelExportInfo(classDef.name.name, _)) + .map(generator.generateTopLevelExportInfo(classDef.name.name, _)) val jsNativeMembers = classDef.jsNativeMembers .map(m => m.name.name -> m.jsNativeLoadSpec).toMap @@ -136,7 +141,7 @@ private[analyzer] object InfoLoader { } private def genMethodInfos(methods: List[MethodDef], - prevMethodInfos: MethodInfos): MethodInfos = { + prevMethodInfos: MethodInfos, generator: Infos.InfoGenerator): MethodInfos = { val builders = Array.fill(MemberNamespace.Count)(Map.newBuilder[MethodName, Infos.MethodInfo]) @@ -144,7 +149,7 @@ private[analyzer] object InfoLoader { val info = prevMethodInfos(method.flags.namespace.ordinal) .get(method.methodName) .filter(_.version.sameVersion(method.version)) - .getOrElse(Infos.generateMethodInfo(method)) + .getOrElse(generator.generateMethodInfo(method)) builders(method.flags.namespace.ordinal) += method.methodName -> info } @@ -153,16 +158,18 @@ private[analyzer] object InfoLoader { } private def genJSCtorInfo(jsCtor: Option[JSConstructorDef], - prevJSCtorInfo: Option[Infos.ReachabilityInfo]): Option[Infos.ReachabilityInfo] = { + prevJSCtorInfo: Option[Infos.ReachabilityInfo], + generator: Infos.InfoGenerator): Option[Infos.ReachabilityInfo] = { jsCtor.map { ctor => prevJSCtorInfo .filter(_.version.sameVersion(ctor.version)) - .getOrElse(Infos.generateJSConstructorInfo(ctor)) + .getOrElse(generator.generateJSConstructorInfo(ctor)) } } private def genJSMethodPropDefInfos(jsMethodProps: List[JSMethodPropDef], - prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo]): List[Infos.ReachabilityInfo] = { + prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo], + generator: Infos.InfoGenerator): List[Infos.ReachabilityInfo] = { /* For JS method and property definitions, we use their index in the list of * `linkedClass.exportedMembers` as their identity. We cannot use their name * because the name itself is a `Tree`. @@ -176,13 +183,13 @@ private[analyzer] object InfoLoader { if (prevJSMethodPropDefInfos.size != jsMethodProps.size) { // Regenerate everything. - jsMethodProps.map(Infos.generateJSMethodPropDefInfo(_)) + jsMethodProps.map(generator.generateJSMethodPropDefInfo(_)) } else { for { (prevInfo, member) <- prevJSMethodPropDefInfos.zip(jsMethodProps) } yield { if (prevInfo.version.sameVersion(member.version)) prevInfo - else Infos.generateJSMethodPropDefInfo(member) + else generator.generateJSMethodPropDefInfo(member) } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala index fe957ca837..90fe76ca07 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala @@ -22,8 +22,7 @@ import org.scalajs.ir.Types._ import org.scalajs.ir.Version import org.scalajs.ir.WellKnownNames._ -import org.scalajs.linker.backend.emitter.Transients._ -import org.scalajs.linker.standard.LinkedTopLevelExport +import org.scalajs.linker.frontend.{LinkTimeEvaluator, LinkTimeProperties} import org.scalajs.linker.standard.ModuleSet.ModuleID object Infos { @@ -184,27 +183,6 @@ object Infos { val methodName: MethodName ) extends MemberReachabilityInfo - def genReferencedFieldClasses(fields: List[AnyFieldDef]): Map[FieldName, ClassName] = { - val builder = Map.newBuilder[FieldName, ClassName] - - fields.foreach { - case FieldDef(flags, FieldIdent(name), _, ftpe) => - if (!flags.namespace.isStatic) { - ftpe match { - case ClassType(cls, _) => - builder += name -> cls - case ArrayType(ArrayTypeRef(ClassRef(cls), _), _) => - builder += name -> cls - case _ => - } - } - case _: JSFieldDef => - // Nothing to do. - } - - builder.result() - } - final class ReachabilityInfoBuilder(version: Version) { import ReachabilityInfoBuilder._ private val byClass = mutable.Map.empty[ClassName, ReachabilityInfoInClassBuilder] @@ -415,8 +393,11 @@ object Infos { def addUsedClassSuperClass(): this.type = setFlag(ReachabilityInfo.FlagUsedClassSuperClass) - def addReferencedLinkTimeProperty(linkTimeProperty: LinkTimeProperty): this.type = { + def markNeedsDesugaring(): this.type = setFlag(ReachabilityInfo.FlagNeedsDesugaring) + + def addReferencedLinkTimeProperty(linkTimeProperty: LinkTimeProperty): this.type = { + markNeedsDesugaring() linkTimeProperties.append((linkTimeProperty.name, linkTimeProperty.tpe)) this } @@ -539,46 +520,71 @@ object Infos { } } - /** Generates the [[MethodInfo]] of a - * [[org.scalajs.ir.Trees.MethodDef Trees.MethodDef]]. - */ - def generateMethodInfo(methodDef: MethodDef): MethodInfo = - new GenInfoTraverser(methodDef.version).generateMethodInfo(methodDef) + final class InfoGenerator(linkTimeProperties: LinkTimeProperties) { + def genReferencedFieldClasses(fields: List[AnyFieldDef]): Map[FieldName, ClassName] = { + val builder = Map.newBuilder[FieldName, ClassName] + + fields.foreach { + case FieldDef(flags, FieldIdent(name), _, ftpe) => + if (!flags.namespace.isStatic) { + ftpe match { + case ClassType(cls, _) => + builder += name -> cls + case ArrayType(ArrayTypeRef(ClassRef(cls), _), _) => + builder += name -> cls + case _ => + } + } + case _: JSFieldDef => + // Nothing to do. + } - /** Generates the [[ReachabilityInfo]] of a - * [[org.scalajs.ir.Trees.JSConstructorDef Trees.JSConstructorDef]]. - */ - def generateJSConstructorInfo(ctorDef: JSConstructorDef): ReachabilityInfo = - new GenInfoTraverser(ctorDef.version).generateJSConstructorInfo(ctorDef) + builder.result() + } - /** Generates the [[ReachabilityInfo]] of a - * [[org.scalajs.ir.Trees.JSMethodDef Trees.JSMethodDef]]. - */ - def generateJSMethodInfo(methodDef: JSMethodDef): ReachabilityInfo = - new GenInfoTraverser(methodDef.version).generateJSMethodInfo(methodDef) + /** Generates the [[MethodInfo]] of a + * [[org.scalajs.ir.Trees.MethodDef Trees.MethodDef]]. + */ + def generateMethodInfo(methodDef: MethodDef): MethodInfo = + new GenInfoTraverser(methodDef.version, linkTimeProperties).generateMethodInfo(methodDef) - /** Generates the [[ReachabilityInfo]] of a - * [[org.scalajs.ir.Trees.JSPropertyDef Trees.JSPropertyDef]]. - */ - def generateJSPropertyInfo(propertyDef: JSPropertyDef): ReachabilityInfo = - new GenInfoTraverser(propertyDef.version).generateJSPropertyInfo(propertyDef) + /** Generates the [[ReachabilityInfo]] of a + * [[org.scalajs.ir.Trees.JSConstructorDef Trees.JSConstructorDef]]. + */ + def generateJSConstructorInfo(ctorDef: JSConstructorDef): ReachabilityInfo = + new GenInfoTraverser(ctorDef.version, linkTimeProperties).generateJSConstructorInfo(ctorDef) - def generateJSMethodPropDefInfo(member: JSMethodPropDef): ReachabilityInfo = member match { - case methodDef: JSMethodDef => generateJSMethodInfo(methodDef) - case propertyDef: JSPropertyDef => generateJSPropertyInfo(propertyDef) - } + /** Generates the [[ReachabilityInfo]] of a + * [[org.scalajs.ir.Trees.JSMethodDef Trees.JSMethodDef]]. + */ + def generateJSMethodInfo(methodDef: JSMethodDef): ReachabilityInfo = + new GenInfoTraverser(methodDef.version, linkTimeProperties).generateJSMethodInfo(methodDef) + + /** Generates the [[ReachabilityInfo]] of a + * [[org.scalajs.ir.Trees.JSPropertyDef Trees.JSPropertyDef]]. + */ + def generateJSPropertyInfo(propertyDef: JSPropertyDef): ReachabilityInfo = + new GenInfoTraverser(propertyDef.version, linkTimeProperties).generateJSPropertyInfo(propertyDef) - /** Generates the [[MethodInfo]] for the top-level exports. */ - def generateTopLevelExportInfo(enclosingClass: ClassName, - topLevelExportDef: TopLevelExportDef): TopLevelExportInfo = { - val info = new GenInfoTraverser(Version.Unversioned) - .generateTopLevelExportInfo(enclosingClass, topLevelExportDef) - new TopLevelExportInfo(info, - ModuleID(topLevelExportDef.moduleID), - topLevelExportDef.topLevelExportName) + def generateJSMethodPropDefInfo(member: JSMethodPropDef): ReachabilityInfo = member match { + case methodDef: JSMethodDef => generateJSMethodInfo(methodDef) + case propertyDef: JSPropertyDef => generateJSPropertyInfo(propertyDef) + } + + /** Generates the [[MethodInfo]] for the top-level exports. */ + def generateTopLevelExportInfo(enclosingClass: ClassName, + topLevelExportDef: TopLevelExportDef): TopLevelExportInfo = { + val info = new GenInfoTraverser(Version.Unversioned, linkTimeProperties) + .generateTopLevelExportInfo(enclosingClass, topLevelExportDef) + new TopLevelExportInfo(info, + ModuleID(topLevelExportDef.moduleID), + topLevelExportDef.topLevelExportName) + } } - private final class GenInfoTraverser(version: Version) extends Traverser { + private final class GenInfoTraverser(version: Version, + linkTimeProperties: LinkTimeProperties) extends Traverser { + private val builder = new ReachabilityInfoBuilder(version) /** Whether we are currently in the body of an `async` closure. @@ -684,6 +690,36 @@ object Infos { // Capture values are in the enclosing scope; not the scope of the closure captureValues.foreach(traverse(_)) + // Do not call super.traverse(), as we must follow a single branch + case LinkTimeIf(cond, thenp, elsep) => + builder.markNeedsDesugaring() + traverse(cond) + LinkTimeEvaluator.tryEvalLinkTimeBooleanExpr(linkTimeProperties, cond) match { + case Some(result) => + if (result) + traverse(thenp) + else + traverse(elsep) + case None => + /* Ignore. Recall that we *assume* here that the ClassDef is + * valid on its own, i.e., it would pass the ClassDefChecker + * (irrespective of whether we actually run that checker). + * + * Under that assumption, the only failure mode for evaluating + * the `cond` is that it refers to a `LinkTimeProperty` that + * does not exist or has the wrong type. In that case, the + * analyzer will report a linking error at least for that + * `LinkTimeProperty` inside the `cond` (which we always + * traverse). + * + * If the assumption is broken and the evaluation failure was + * due to an ill-formed or ill-typed `cond`, then Desugar will + * eventually crash (with a message suggesting to enable checking + * the IR). + */ + () + } + // In all other cases, we'll have to call super.traverse() case _ => tree match { @@ -753,12 +789,12 @@ object Infos { import BinaryOp._ op match { - case Int_/ | Int_% => + case Int_/ | Int_% | Int_unsigned_/ | Int_unsigned_% => rhs match { case IntLiteral(r) if r != 0 => case _ => builder.addUsedIntLongDivModByMaybeZero() } - case Long_/ | Long_% => + case Long_/ | Long_% | Long_unsigned_/ | Long_unsigned_% => rhs match { case LongLiteral(r) if r != 0L => case _ => builder.addUsedIntLongDivModByMaybeZero() diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index 6fb37ce343..bc8610d0c0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -101,6 +101,8 @@ private[emitter] object CoreJSLib { private val StringRef = globalRef("String") private val MathRef = globalRef("Math") private val NumberRef = globalRef("Number") + private val DataViewRef = globalRef("DataView") + private val ArrayBufferRef = globalRef("ArrayBuffer") private val TypeErrorRef = globalRef("TypeError") private def BigIntRef = globalRef("BigInt") private val SymbolRef = globalRef("Symbol") @@ -190,6 +192,25 @@ private[emitter] object CoreJSLib { Return((al * bl) + (((ah * bl + al * bh) << 16) >>> 0) | 0) )) + case Clz32Builtin => + // See Hacker's Delight, Section 5-3 + val x = varRef("x") + val r = varRef("r") + genArrowFunction(paramList(x), { + If(x === 0, { + Return(32) + }, { + Block( + let(r, 1), + If((x & 0xffff0000) === 0, Block(x := x << 16, r := r + 16), Skip()), + If((x & 0xff000000) === 0, Block(x := x << 8, r := r + 8), Skip()), + If((x & 0xf0000000) === 0, Block(x := x << 4, r := r + 4), Skip()), + If((x & 0xc0000000) === 0, Block(x := x << 2, r := r + 2), Skip()), + Return(r + (x >> 31)) + ) + }) + }) + case FroundBuiltin => val v = varRef("v") val Float32ArrayRef = globalRef("Float32Array") @@ -874,18 +895,54 @@ private[emitter] object CoreJSLib { def wrapBigInt64(tree: Tree): Tree = Apply(genIdentBracketSelect(BigIntRef, "asIntN"), 64 :: tree :: Nil) + /* Defines a core function of 1 argument `x` that uses the `fpBitsDataView` + * global var. When linking for ES 2015+, the provided body is always + * used, as `fpBitsDataView` is known to exist. When linking for 5.1, + * a polyfill from `org.scalajs.linker.runtime.FloatingPointBitsPolyfills` + * is used when `fpBitsDataView` is `null`. + * + * The `body` function receives `x` and `fpBitsDataView` as arguments, + * in that order. + */ + def defineFloatingPointBitsFunctionOrPolyfill(name: VarField, + polyfillMethod: MethodName)(body: (VarRef, VarRef) => Tree): List[Tree] = { + + val dataView = varRef("dataView") + val dataViewConst = const(dataView, globalVar(VarField.fpBitsDataView, CoreVar)) + + if (esVersion >= ESVersion.ES2015) { + defineFunction1(name) { x => + Block( + dataViewConst, + body(x, dataView) + ) + } + } else { + val x = varRef("x") + + extractWithGlobals(globalVarDef(name, CoreVar, { + If(globalVar(VarField.fpBitsDataView, CoreVar) !== Null(), { + genArrowFunction(paramList(x), { + Block( + dataViewConst, + body(x, dataView) + ) + }) + }, { + genArrowFunction(paramList(x), { + Return(Apply(globalVar(VarField.s, (FloatingPointBitsPolyfillsClass, polyfillMethod)), List(x))) + }) + }) + })) + } + } + condDefs(shouldDefineIntLongDivModFunctions)( - defineFunction2(VarField.intDiv) { (x, y) => + defineFunction1(VarField.checkIntDivisor) { y => If(y === 0, throwDivByZero, { - Return((x / y) | 0) + Return(y) }) - } ::: - defineFunction2(VarField.intMod) { (x, y) => - If(y === 0, throwDivByZero, { - Return((x % y) | 0) - }) - } ::: - Nil + } ) ::: defineFunction1(VarField.doubleToInt) { x => Return(If(x > 2147483647, 2147483647, If(x < -2147483648, -2147483648, x | 0))) @@ -909,19 +966,34 @@ private[emitter] object CoreJSLib { } ) ::: condDefs(allowBigIntsForLongs && shouldDefineIntLongDivModFunctions)( - defineFunction2(VarField.longDiv) { (x, y) => - If(y === bigInt(0), throwDivByZero, { - Return(wrapBigInt64(x / y)) - }) - } ::: - defineFunction2(VarField.longMod) { (x, y) => + defineFunction1(VarField.checkLongDivisor) { y => If(y === bigInt(0), throwDivByZero, { - Return(wrapBigInt64(x % y)) + Return(y) }) - } ::: - Nil + } ) ::: condDefs(allowBigIntsForLongs)( + defineFunction1(VarField.longClz) { x => + // (Math.clz32 o Number)(bigIntArg), i.e., Math.clz32(Number(bigIntArg)) + def clz32_o_Number(bigIntArg: Tree): Tree = { + genCallPolyfillableBuiltin(PolyfillableBuiltin.Clz32Builtin, + Apply(NumberRef, List(bigIntArg))) + } + + val hi = varRef("hi") + + Block( + const(hi, x >> bigInt(32)), + Return { + If(hi !== bigInt(0L), { + clz32_o_Number(hi) + }, { + int(32) + clz32_o_Number(x) + }) + } + ) + } ::: + defineFunction1(VarField.doubleToLong)(x => Return { If(x < double(-9223372036854775808.0), { // -2^63 bigInt(-9223372036854775808L) @@ -956,7 +1028,51 @@ private[emitter] object CoreJSLib { Return(genCallPolyfillableBuiltin(FroundBuiltin, If(x < bigInt(0L), -absR, absR))) ) } - ) + ) ::: + extractWithGlobals(globalVarDef(VarField.fpBitsDataView, CoreVar, { + val newDataView = New(DataViewRef, List(New(ArrayBufferRef, List(8)))) + if (esVersion >= ESVersion.ES2015) { + newDataView + } else { + If(typeof(DataViewRef) !== str("undefined"), { + newDataView + }, { + Null() + }) + } + })) ::: + defineFloatingPointBitsFunctionOrPolyfill(VarField.floatToBits, floatToBits) { (x, fpBitsDataView) => + Block( + Apply(genIdentBracketSelect(fpBitsDataView, "setFloat32"), List(0, x, bool(true))), + Return(Apply(genIdentBracketSelect(fpBitsDataView, "getInt32"), List(0, bool(true)))) + ) + } ::: + defineFloatingPointBitsFunctionOrPolyfill(VarField.floatFromBits, floatFromBits) { (x, fpBitsDataView) => + Block( + Apply(genIdentBracketSelect(fpBitsDataView, "setInt32"), List(0, x, bool(true))), + Return(Apply(genIdentBracketSelect(fpBitsDataView, "getFloat32"), List(0, bool(true)))) + ) + } ::: + defineFloatingPointBitsFunctionOrPolyfill(VarField.doubleToBits, doubleToBits) { (x, fpBitsDataView) => + if (allowBigIntsForLongs) { + Block( + Apply(genIdentBracketSelect(fpBitsDataView, "setFloat64"), List(0, x, bool(true))), + Return(Apply(genIdentBracketSelect(fpBitsDataView, "getBigInt64"), List(0, bool(true)))) + ) + } else { + Return(genLongApplyStatic(LongImpl.fromDoubleBits, x, fpBitsDataView)) + } + } ::: + defineFloatingPointBitsFunctionOrPolyfill(VarField.doubleFromBits, doubleFromBits) { (x, fpBitsDataView) => + if (allowBigIntsForLongs) { + Block( + Apply(genIdentBracketSelect(fpBitsDataView, "setBigInt64"), List(0, x, bool(true))), + Return(Apply(genIdentBracketSelect(fpBitsDataView, "getFloat64"), List(0, bool(true)))) + ) + } else { + Return(genLongApplyStatic(LongImpl.bitsToDouble, x, fpBitsDataView)) + } + } } private def defineES2015LikeHelpers(): List[Tree] = ( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index b625c51c12..77e0c1ba39 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -1437,8 +1437,18 @@ object Emitter { multiple( instanceTests(LongImpl.RuntimeLongClass), instantiateClass(LongImpl.RuntimeLongClass, LongImpl.AllConstructors.toList), - callMethods(LongImpl.RuntimeLongClass, LongImpl.AllMethods.toList), - callOnModule(LongImpl.RuntimeLongModuleClass, LongImpl.AllModuleMethods.toList) + callMethods(LongImpl.RuntimeLongClass, LongImpl.BoxedLongMethods.toList), + callStaticMethods(LongImpl.RuntimeLongClass, LongImpl.OperatorMethods.toList) + ) + }, + + cond(config.coreSpec.esFeatures.esVersion < ESVersion.ES2015) { + val cls = FloatingPointBitsPolyfillsClass + multiple( + callStaticMethod(cls, floatToBits), + callStaticMethod(cls, floatFromBits), + callStaticMethod(cls, doubleToBits), + callStaticMethod(cls, doubleFromBits) ) } ) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala index 02e46fd548..3bf4e8984a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala @@ -25,6 +25,9 @@ private[emitter] object EmitterNames { val UndefinedBehaviorErrorClass = ClassName("org.scalajs.linker.runtime.UndefinedBehaviorError") + val FloatingPointBitsPolyfillsClass = + ClassName("org.scalajs.linker.runtime.FloatingPointBitsPolyfills") + // Field names val exceptionFieldName = FieldName(JavaScriptExceptionClass, SimpleFieldName("exception")) @@ -43,4 +46,9 @@ private[emitter] object EmitterNames { val getNameMethodName = MethodName("getName", Nil, ClassRef(BoxedStringClass)) val getSuperclassMethodName = MethodName("getSuperclass", Nil, ClassRef(ClassClass)) + + val floatToBits = MethodName("floatToBits", List(FloatRef), IntRef) + val floatFromBits = MethodName("floatFromBits", List(IntRef), DoubleRef) // yes, Double + val doubleToBits = MethodName("doubleToBits", List(DoubleRef), LongRef) + val doubleFromBits = MethodName("doubleFromBits", List(LongRef), DoubleRef) } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index e5c444e342..e3f696248c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -1266,8 +1266,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def test(tree: Tree): Boolean = tree match { // Atomic expressions - case _: Literal => true - case _: JSNewTarget => true + case _: Literal => true + case _: JSNewTarget => true + case Transient(GetFPBitsDataView) => true // Vars (side-effect free, pure if immutable) case VarRef(name) => @@ -1286,12 +1287,14 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { allowSideEffects && test(lhs) // Division and modulo, preserve pureness unless they can divide by 0 - case BinaryOp(BinaryOp.Int_/ | BinaryOp.Int_%, lhs, rhs) if !allowSideEffects => + case BinaryOp(BinaryOp.Int_/ | BinaryOp.Int_% | BinaryOp.Int_unsigned_/ | BinaryOp.Int_unsigned_%, lhs, rhs) + if !allowSideEffects => rhs match { case IntLiteral(r) if r != 0 => test(lhs) case _ => false } - case BinaryOp(BinaryOp.Long_/ | BinaryOp.Long_%, lhs, rhs) if !allowSideEffects => + case BinaryOp(BinaryOp.Long_/ | BinaryOp.Long_% | BinaryOp.Long_unsigned_/ | BinaryOp.Long_unsigned_%, lhs, rhs) + if !allowSideEffects => rhs match { case LongLiteral(r) if r != 0L => test(lhs) case _ => false @@ -2205,6 +2208,13 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def or0(tree: js.Tree): js.Tree = js.BinaryOp(JSBinaryOp.|, tree, js.IntLiteral(0)) + def shr0(tree: js.Tree): js.Tree = tree match { + case js.IntLiteral(value) => + js.UintLiteral(value) + case _ => + js.BinaryOp(JSBinaryOp.>>>, tree, js.IntLiteral(0)) + } + def bigIntShiftRhs(tree: js.Tree): js.Tree = { tree match { case js.IntLiteral(v) => @@ -2389,7 +2399,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) js.Apply(genGlobalVarRef("BigInt"), List(newLhs)) else - genLongModuleApply(LongImpl.fromInt, newLhs) + genLongApplyStatic(LongImpl.fromInt, newLhs) // Narrowing conversions case IntToChar => @@ -2406,7 +2416,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) js.Apply(genGlobalVarRef("Number"), List(wrapBigInt32(newLhs))) else - genApply(newLhs, LongImpl.toInt) + genLongApplyStatic(LongImpl.toInt, newLhs) case DoubleToInt => genCallHelper(VarField.doubleToInt, newLhs) case DoubleToFloat => @@ -2417,19 +2427,19 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) js.Apply(genGlobalVarRef("Number"), List(newLhs)) else - genApply(newLhs, LongImpl.toDouble) + genLongApplyStatic(LongImpl.toDouble, newLhs) case DoubleToLong => if (useBigIntForLongs) genCallHelper(VarField.doubleToLong, newLhs) else - genLongModuleApply(LongImpl.fromDouble, newLhs) + genLongApplyStatic(LongImpl.fromDouble, newLhs) // Long -> Float (neither widening nor narrowing) case LongToFloat => if (useBigIntForLongs) genCallHelper(VarField.longToFloat, newLhs) else - genApply(newLhs, LongImpl.toFloat) + genLongApplyStatic(LongImpl.toFloat, newLhs) // String.length case String_length => @@ -2515,6 +2525,31 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genIsInstanceOfClass(newLhs, JavaScriptExceptionClass), genSelect(newLhs, FieldIdent(exceptionFieldName)), newLhs) + + // Floating point bit manipulation + case Float_toBits => + genCallHelper(VarField.floatToBits, newLhs) + case Float_fromBits => + genCallHelper(VarField.floatFromBits, newLhs) + case Double_toBits => + genCallHelper(VarField.doubleToBits, newLhs) + case Double_fromBits => + genCallHelper(VarField.doubleFromBits, newLhs) + + // clz + case Int_clz => + genCallPolyfillableBuiltin(PolyfillableBuiltin.Clz32Builtin, newLhs) + case Long_clz => + if (useBigIntForLongs) + genCallHelper(VarField.longClz, newLhs) + else + genLongApplyStatic(LongImpl.clz, newLhs) + + case UnsignedIntToLong => + if (useBigIntForLongs) + js.Apply(genGlobalVarRef("BigInt"), List(shr0(newLhs))) + else + genLongApplyStatic(LongImpl.fromUnsignedInt, newLhs) } case BinaryOp(op, lhs, rhs) => @@ -2620,20 +2655,17 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } case Int_* => genCallPolyfillableBuiltin(ImulBuiltin, newLhs, newRhs) - case Int_/ => - rhs match { - case IntLiteral(r) if r != 0 => - or0(js.BinaryOp(JSBinaryOp./, newLhs, newRhs)) - case _ => - genCallHelper(VarField.intDiv, newLhs, newRhs) - } - case Int_% => - rhs match { - case IntLiteral(r) if r != 0 => - or0(js.BinaryOp(JSBinaryOp.%, newLhs, newRhs)) - case _ => - genCallHelper(VarField.intMod, newLhs, newRhs) + case Int_/ | Int_% | Int_unsigned_/ | Int_unsigned_% => + val newRhs1 = rhs match { + case IntLiteral(r) if r != 0 => newRhs + case _ => genCallHelper(VarField.checkIntDivisor, newRhs) } + or0((op: @switch) match { + case Int_/ => js.BinaryOp(JSBinaryOp./, newLhs, newRhs1) + case Int_% => js.BinaryOp(JSBinaryOp.%, newLhs, newRhs1) + case Int_unsigned_/ => js.BinaryOp(JSBinaryOp./, shr0(newLhs), shr0(newRhs1)) + case Int_unsigned_% => js.BinaryOp(JSBinaryOp.%, shr0(newLhs), shr0(newRhs1)) + }) case Int_| => js.BinaryOp(JSBinaryOp.|, newLhs, newRhs) case Int_& => js.BinaryOp(JSBinaryOp.&, newLhs, newRhs) @@ -2655,117 +2687,123 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs)) else - genApply(newLhs, LongImpl.+, newRhs) + genLongApplyStatic(LongImpl.add, newLhs, newRhs) case Long_- => - lhs match { - case LongLiteral(0L) => - if (useBigIntForLongs) + if (useBigIntForLongs) { + lhs match { + case LongLiteral(0L) => wrapBigInt64(js.UnaryOp(JSUnaryOp.-, newRhs)) - else - genApply(newRhs, LongImpl.UNARY_-) - case _ => - if (useBigIntForLongs) + case _ => wrapBigInt64(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs)) - else - genApply(newLhs, LongImpl.-, newRhs) + } + } else { + /* RuntimeLong does not have a dedicated method for 0L - b. + * The regular expansion done by the optimizer for the binary + * form is already optimal. + * So we don't special-case it here either. + */ + genLongApplyStatic(LongImpl.sub, newLhs, newRhs) } case Long_* => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.*, newLhs, newRhs)) else - genApply(newLhs, LongImpl.*, newRhs) - case Long_/ => + genLongApplyStatic(LongImpl.mul, newLhs, newRhs) + case Long_/ | Long_% | Long_unsigned_/ | Long_unsigned_% => if (useBigIntForLongs) { - rhs match { - case LongLiteral(r) if r != 0L => - wrapBigInt64(js.BinaryOp(JSBinaryOp./, newLhs, newRhs)) - case _ => - genCallHelper(VarField.longDiv, newLhs, newRhs) + val newRhs1 = rhs match { + case LongLiteral(r) if r != 0L => newRhs + case _ => genCallHelper(VarField.checkLongDivisor, newRhs) } + wrapBigInt64((op: @switch) match { + case Long_/ => js.BinaryOp(JSBinaryOp./, newLhs, newRhs1) + case Long_% => js.BinaryOp(JSBinaryOp.%, newLhs, newRhs1) + case Long_unsigned_/ => js.BinaryOp(JSBinaryOp./, wrapBigIntU64(newLhs), wrapBigIntU64(newRhs1)) + case Long_unsigned_% => js.BinaryOp(JSBinaryOp.%, wrapBigIntU64(newLhs), wrapBigIntU64(newRhs1)) + }) } else { - genApply(newLhs, LongImpl./, newRhs) - } - case Long_% => - if (useBigIntForLongs) { - rhs match { - case LongLiteral(r) if r != 0L => - wrapBigInt64(js.BinaryOp(JSBinaryOp.%, newLhs, newRhs)) - case _ => - genCallHelper(VarField.longMod, newLhs, newRhs) + // The zero divisor check is performed by the implementation methods + val implMethodName = (op: @switch) match { + case Long_/ => LongImpl.divide + case Long_% => LongImpl.remainder + case Long_unsigned_/ => LongImpl.divideUnsigned + case Long_unsigned_% => LongImpl.remainderUnsigned } - } else { - genApply(newLhs, LongImpl.%, newRhs) + genLongApplyStatic(implMethodName, newLhs, newRhs) } case Long_| => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.|, newLhs, newRhs)) else - genApply(newLhs, LongImpl.|, newRhs) + genLongApplyStatic(LongImpl.or, newLhs, newRhs) case Long_& => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.&, newLhs, newRhs)) else - genApply(newLhs, LongImpl.&, newRhs) + genLongApplyStatic(LongImpl.and, newLhs, newRhs) case Long_^ => - lhs match { - case LongLiteral(-1L) => - if (useBigIntForLongs) + if (useBigIntForLongs) { + lhs match { + case LongLiteral(-1L) => wrapBigInt64(js.UnaryOp(JSUnaryOp.~, newRhs)) - else - genApply(newRhs, LongImpl.UNARY_~) - case _ => - if (useBigIntForLongs) + case _ => wrapBigInt64(js.BinaryOp(JSBinaryOp.^, newLhs, newRhs)) - else - genApply(newLhs, LongImpl.^, newRhs) + } + } else { + /* RuntimeLong does not have a dedicated method for -1L ^ b. + * The regular expansion done by the optimizer for the binary + * form is already optimal. + * So we don't special-case it here either. + */ + genLongApplyStatic(LongImpl.xor, newLhs, newRhs) } case Long_<< => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.<<, newLhs, bigIntShiftRhs(newRhs))) else - genApply(newLhs, LongImpl.<<, newRhs) + genLongApplyStatic(LongImpl.shl, newLhs, newRhs) case Long_>>> => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.>>, wrapBigIntU64(newLhs), bigIntShiftRhs(newRhs))) else - genApply(newLhs, LongImpl.>>>, newRhs) + genLongApplyStatic(LongImpl.shr, newLhs, newRhs) case Long_>> => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.>>, newLhs, bigIntShiftRhs(newRhs))) else - genApply(newLhs, LongImpl.>>, newRhs) + genLongApplyStatic(LongImpl.sar, newLhs, newRhs) case Long_== => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.===, newLhs, newRhs) else - genApply(newLhs, LongImpl.===, newRhs) + genLongApplyStatic(LongImpl.equals_, newLhs, newRhs) case Long_!= => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.!==, newLhs, newRhs) else - genApply(newLhs, LongImpl.!==, newRhs) + genLongApplyStatic(LongImpl.notEquals, newLhs, newRhs) case Long_< => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.<, newLhs, newRhs) else - genApply(newLhs, LongImpl.<, newRhs) + genLongApplyStatic(LongImpl.lt, newLhs, newRhs) case Long_<= => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.<=, newLhs, newRhs) else - genApply(newLhs, LongImpl.<=, newRhs) + genLongApplyStatic(LongImpl.le, newLhs, newRhs) case Long_> => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.>, newLhs, newRhs) else - genApply(newLhs, LongImpl.>, newRhs) + genLongApplyStatic(LongImpl.gt, newLhs, newRhs) case Long_>= => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.>=, newLhs, newRhs) else - genApply(newLhs, LongImpl.>=, newRhs) + genLongApplyStatic(LongImpl.ge, newLhs, newRhs) case Float_+ => genFround(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs)) case Float_- => genFround(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs)) @@ -2815,6 +2853,32 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Apply(extractClassData(lhs, newLhs) DOT cpn.cast, newRhs :: Nil) case Class_newArray => js.Apply(extractClassData(lhs, newLhs) DOT cpn.newArray, newRhs :: Nil) + + case Int_unsigned_< => js.BinaryOp(JSBinaryOp.<, shr0(newLhs), shr0(newRhs)) + case Int_unsigned_<= => js.BinaryOp(JSBinaryOp.<=, shr0(newLhs), shr0(newRhs)) + case Int_unsigned_> => js.BinaryOp(JSBinaryOp.>, shr0(newLhs), shr0(newRhs)) + case Int_unsigned_>= => js.BinaryOp(JSBinaryOp.>=, shr0(newLhs), shr0(newRhs)) + + case Long_unsigned_< => + if (useBigIntForLongs) + js.BinaryOp(JSBinaryOp.<, wrapBigIntU64(newLhs), wrapBigIntU64(newRhs)) + else + genLongApplyStatic(LongImpl.ltu, newLhs, newRhs) + case Long_unsigned_<= => + if (useBigIntForLongs) + js.BinaryOp(JSBinaryOp.<=, wrapBigIntU64(newLhs), wrapBigIntU64(newRhs)) + else + genLongApplyStatic(LongImpl.leu, newLhs, newRhs) + case Long_unsigned_> => + if (useBigIntForLongs) + js.BinaryOp(JSBinaryOp.>, wrapBigIntU64(newLhs), wrapBigIntU64(newRhs)) + else + genLongApplyStatic(LongImpl.gtu, newLhs, newRhs) + case Long_unsigned_>= => + if (useBigIntForLongs) + js.BinaryOp(JSBinaryOp.>=, wrapBigIntU64(newLhs), wrapBigIntU64(newRhs)) + else + genLongApplyStatic(LongImpl.geu, newLhs, newRhs) } case NewArray(typeRef, length) => @@ -2879,6 +2943,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ObjectClassName(obj)) => genCallHelper(VarField.objectClassName, transformExprNoChar(obj)) + case Transient(GetFPBitsDataView) => + globalVar(VarField.fpBitsDataView, CoreVar) + case Transient(ArrayToTypedArray(expr, primRef)) => val value = transformExprNoChar(checkNotNull(expr)) val valueUnderlying = genSyntheticPropSelect(value, SyntheticProperty.u) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/LongImpl.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/LongImpl.scala index e4059324e0..98f1b8cccf 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/LongImpl.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/LongImpl.scala @@ -18,85 +18,107 @@ import org.scalajs.ir.WellKnownNames._ private[linker] object LongImpl { final val RuntimeLongClass = ClassName("org.scalajs.linker.runtime.RuntimeLong") - final val RuntimeLongModuleClass = ClassName("org.scalajs.linker.runtime.RuntimeLong$") final val lo = MethodName("lo", Nil, IntRef) final val hi = MethodName("hi", Nil, IntRef) private final val RTLongRef = ClassRef(RuntimeLongClass) private final val OneRTLongRef = RTLongRef :: Nil + private final val TwoRTLongRefs = RTLongRef :: OneRTLongRef def unaryOp(name: String): MethodName = - MethodName(name, Nil, RTLongRef) + MethodName(name, OneRTLongRef, RTLongRef) def binaryOp(name: String): MethodName = - MethodName(name, OneRTLongRef, RTLongRef) + MethodName(name, TwoRTLongRefs, RTLongRef) def shiftOp(name: String): MethodName = - MethodName(name, List(IntRef), RTLongRef) + MethodName(name, List(RTLongRef, IntRef), RTLongRef) def compareOp(name: String): MethodName = - MethodName(name, OneRTLongRef, BooleanRef) - - final val UNARY_- = unaryOp("unary_$minus") - final val UNARY_~ = unaryOp("unary_$tilde") - - final val + = binaryOp("$plus") - final val - = binaryOp("$minus") - final val * = binaryOp("$times") - final val / = binaryOp("$div") - final val % = binaryOp("$percent") - - final val | = binaryOp("$bar") - final val & = binaryOp("$amp") - final val ^ = binaryOp("$up") - - final val << = shiftOp("$less$less") - final val >>> = shiftOp("$greater$greater$greater") - final val >> = shiftOp("$greater$greater") - - final val === = compareOp("equals") - final val !== = compareOp("notEquals") - final val < = compareOp("$less") - final val <= = compareOp("$less$eq") - final val > = compareOp("$greater") - final val >= = compareOp("$greater$eq") - - final val toInt = MethodName("toInt", Nil, IntRef) - final val toFloat = MethodName("toFloat", Nil, FloatRef) - final val toDouble = MethodName("toDouble", Nil, DoubleRef) - - final val byteValue = MethodName("byteValue", Nil, ByteRef) - final val shortValue = MethodName("shortValue", Nil, ShortRef) - final val intValue = MethodName("intValue", Nil, IntRef) - final val longValue = MethodName("longValue", Nil, LongRef) - final val floatValue = MethodName("floatValue", Nil, FloatRef) - final val doubleValue = MethodName("doubleValue", Nil, DoubleRef) - - final val toString_ = MethodName("toString", Nil, ClassRef(BoxedStringClass)) - final val equals_ = MethodName("equals", List(ClassRef(ObjectClass)), BooleanRef) - final val hashCode_ = MethodName("hashCode", Nil, IntRef) - final val compareTo = MethodName("compareTo", List(ClassRef(BoxedLongClass)), IntRef) - final val compareToO = MethodName("compareTo", List(ClassRef(ObjectClass)), IntRef) - - private val OperatorMethods = Set( - UNARY_-, UNARY_~, this.+, this.-, *, /, %, |, &, ^, <<, >>>, >>, - ===, !==, <, <=, >, >=, toInt, toFloat, toDouble) - - private val BoxedLongMethods = Set( + MethodName(name, TwoRTLongRefs, BooleanRef) + + // Instance methods that we need to reach as part of the jl.Long boxing + + private final val byteValue = MethodName("byteValue", Nil, ByteRef) + private final val shortValue = MethodName("shortValue", Nil, ShortRef) + private final val intValue = MethodName("intValue", Nil, IntRef) + private final val longValue = MethodName("longValue", Nil, LongRef) + private final val floatValue = MethodName("floatValue", Nil, FloatRef) + private final val doubleValue = MethodName("doubleValue", Nil, DoubleRef) + + private final val equalsO = MethodName("equals", List(ClassRef(ObjectClass)), BooleanRef) + private final val hashCode_ = MethodName("hashCode", Nil, IntRef) + private final val compareTo = MethodName("compareTo", List(ClassRef(BoxedLongClass)), IntRef) + private final val compareToO = MethodName("compareTo", List(ClassRef(ObjectClass)), IntRef) + + val BoxedLongMethods = Set( byteValue, shortValue, intValue, longValue, floatValue, doubleValue, - equals_, hashCode_, compareTo, compareToO) + equalsO, hashCode_, compareTo, compareToO) - val AllMethods = OperatorMethods ++ BoxedLongMethods + // Operator methods - // Methods used for intrinsics + final val add = binaryOp("add") + final val sub = binaryOp("sub") + final val mul = binaryOp("mul") + final val divide = binaryOp("divide") + final val remainder = binaryOp("remainder") - final val compareToRTLong = MethodName("compareTo", List(RTLongRef), IntRef) - final val divideUnsigned = binaryOp("divideUnsigned") + final val divideUnsigned = binaryOp("divideUnsigned") final val remainderUnsigned = binaryOp("remainderUnsigned") + final val or = binaryOp("or") + final val and = binaryOp("and") + final val xor = binaryOp("xor") + + final val shl = shiftOp("shl") + final val shr = shiftOp("shr") + final val sar = shiftOp("sar") + + final val equals_ = compareOp("equals") + final val notEquals = compareOp("notEquals") + final val lt = compareOp("lt") + final val le = compareOp("le") + final val gt = compareOp("gt") + final val ge = compareOp("ge") + final val ltu = compareOp("ltu") + final val leu = compareOp("leu") + final val gtu = compareOp("gtu") + final val geu = compareOp("geu") + + final val toInt = MethodName("toInt", OneRTLongRef, IntRef) + final val toFloat = MethodName("toFloat", OneRTLongRef, FloatRef) + final val toDouble = MethodName("toDouble", OneRTLongRef, DoubleRef) + final val bitsToDouble = MethodName("bitsToDouble", List(RTLongRef, ObjectRef), DoubleRef) + final val clz = MethodName("clz", OneRTLongRef, IntRef) + + final val fromInt = MethodName("fromInt", List(IntRef), RTLongRef) + final val fromUnsignedInt = MethodName("fromUnsignedInt", List(IntRef), RTLongRef) + final val fromDouble = MethodName("fromDouble", List(DoubleRef), RTLongRef) + final val fromDoubleBits = MethodName("fromDoubleBits", List(DoubleRef, ObjectRef), RTLongRef) + + val OperatorMethods = Set( + add, sub, mul, + divide, remainder, divideUnsigned, remainderUnsigned, + or, and, xor, shl, shr, sar, + equals_, notEquals, lt, le, gt, ge, ltu, leu, gtu, geu, + toInt, toFloat, toDouble, bitsToDouble, clz, + fromInt, fromUnsignedInt, fromDouble, fromDoubleBits + ) + + // Methods used for intrinsics + + final val toString_ = MethodName("toString", OneRTLongRef, ClassRef(BoxedStringClass)) + + final val compare = MethodName("compare", TwoRTLongRefs, IntRef) + + final val multiplyFull = MethodName("multiplyFull", List(IntRef, IntRef), RTLongRef) + val AllIntrinsicMethods = Set( - compareToRTLong, divideUnsigned, remainderUnsigned) + toString_, + compare, + multiplyFull + ) // Constructors @@ -105,14 +127,6 @@ private[linker] object LongImpl { val AllConstructors = Set( initFromParts) - // Methods on the companion - - final val fromInt = MethodName("fromInt", List(IntRef), RTLongRef) - final val fromDouble = MethodName("fromDouble", List(DoubleRef), RTLongRef) - - val AllModuleMethods = Set( - fromInt, fromDouble) - // Extract the parts to give to the initFromParts constructor def extractParts(value: Long): (Int, Int) = diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala index ffb1d57bbe..d6ab128f25 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala @@ -63,7 +63,7 @@ private[backend] final class NameGen { cache.put(ObjectClass, "O") cache.put(BoxedStringClass, "T") cache.put(LongImpl.RuntimeLongClass, "RTLong") - cache.put(LongImpl.RuntimeLongModuleClass, "RTLong$") + cache.put(LongImpl.RuntimeLongClass.withSuffix("$"), "RTLong$") cache } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala index 908d264a9f..43111d8b3c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala @@ -21,6 +21,7 @@ private[emitter] object PolyfillableBuiltin { lazy val All: List[PolyfillableBuiltin] = List( ObjectIsBuiltin, ImulBuiltin, + Clz32Builtin, FroundBuiltin, PrivateSymbolBuiltin, GetOwnPropertyDescriptorsBuiltin @@ -36,6 +37,7 @@ private[emitter] object PolyfillableBuiltin { case object ObjectIsBuiltin extends NamespacedBuiltin("Object", "is", VarField.is, ESVersion.ES2015) case object ImulBuiltin extends NamespacedBuiltin("Math", "imul", VarField.imul, ESVersion.ES2015) + case object Clz32Builtin extends NamespacedBuiltin("Math", "clz32", VarField.clz32, ESVersion.ES2015) case object FroundBuiltin extends NamespacedBuiltin("Math", "fround", VarField.fround, ESVersion.ES2015) case object PrivateSymbolBuiltin extends GlobalVarBuiltin("Symbol", VarField.privateJSFieldSymbol, ESVersion.ES2015) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala index 73ac6c96c9..09514782bb 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala @@ -241,13 +241,10 @@ private[emitter] final class SJSGen( globalVar(VarField.bC0, CoreVar) } - def genLongModuleApply(methodName: MethodName, args: Tree*)( + def genLongApplyStatic(methodName: MethodName, args: Tree*)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { - import TreeDSL._ - genApply( - genLoadModule(LongImpl.RuntimeLongModuleClass), methodName, - args.toList) + Apply(globalVar(VarField.s, (LongImpl.RuntimeLongClass, methodName)), args.toList) } def usesUnderlyingTypedArray(elemTypeRef: NonArrayTypeRef): Boolean = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala index 00301771cc..b1ac1c10a6 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala @@ -156,6 +156,22 @@ object Transients { } } + /** Gets the unique instance of `DataView` used for floating point bit manipulation. + * + * When linking for ES 5.1, the resulting value can be `null`. + */ + final case object GetFPBitsDataView extends Transient.Value { + val tpe: Type = AnyType + + def traverse(traverser: Traverser): Unit = () + + def transform(transformer: Transformer)(implicit pos: Position): Tree = + Transient(this) + + def printIR(out: IRTreePrinter): Unit = + out.print("$fpBitsDataView") + } + /** Copies a primitive `Array` into a new appropriate `TypedArray`. * * This node accepts `null` values for `expr`. Its implementation takes care diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala index 44193542b9..9ce22ed2aa 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala @@ -146,6 +146,9 @@ private[emitter] object VarField { /** Long zero. */ final val L0 = mk("$L0") + /** DataView for floating point bit manipulation. */ + final val fpBitsDataView = mk("$fpBitsDataView") + /** Dispatchers. */ final val dp = mk("$dp") @@ -256,23 +259,27 @@ private[emitter] object VarField { // Arithmetic Call Helpers - final val intDiv = mk("$intDiv") - - final val intMod = mk("$intMod") + final val checkIntDivisor = mk("$checkIntDivisor") - final val longToFloat = mk("$longToFloat") + final val checkLongDivisor = mk("$checkLongDivisor") - final val longDiv = mk("$longDiv") + final val longClz = mk("$longClz") - final val longMod = mk("$longMod") + final val longToFloat = mk("$longToFloat") final val doubleToLong = mk("$doubleToLong") final val doubleToInt = mk("$doubleToInt") + final val floatToBits = mk("$floatToBits") + final val floatFromBits = mk("$floatFromBits") + final val doubleToBits = mk("$doubleToBits") + final val doubleFromBits = mk("$doubleFromBits") + // Polyfills final val imul = mk("$imul") + final val clz32 = mk("$clz32") final val fround = mk("$fround") final val privateJSFieldSymbol = mk("$privateJSFieldSymbol") final val getOwnPropertyDescriptors = mk("$getOwnPropertyDescriptors") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index d4fb5f2284..0d9420dc41 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -371,7 +371,7 @@ object Printers { case DotSelect(qualifier, item) => qualifier match { - case _:IntLiteral | _:DoubleLiteral => + case _:IntLiteral | _:UintLiteral | _:DoubleLiteral => print("(") print(qualifier) print(")") @@ -552,6 +552,10 @@ object Printers { } printSeparatorIfStat() + case UintLiteral(value) => + print(Integer.toUnsignedString(value)) + printSeparatorIfStat() + case DoubleLiteral(value) => if (value == 0 && 1 / value < 0) { print("(-0)") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala index 0ed4501d8f..1482d5e478 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala @@ -416,6 +416,9 @@ object Trees { sealed case class IntLiteral(value: Int)(implicit val pos: Position) extends Literal + sealed case class UintLiteral(value: Int)(implicit val pos: Position) + extends Literal + sealed case class DoubleLiteral(value: Double)(implicit val pos: Position) extends Literal diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala index a86c55909e..a944df20d8 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala @@ -634,7 +634,8 @@ private class FunctionEmitter private ( // Transients (only generated by the optimizer) case t: Transient => genTransient(t) - case _:JSSuperConstructorCall | _:LinkTimeProperty | _:NewLambda => + case _:JSSuperConstructorCall | _:LinkTimeProperty | _:LinkTimeIf | + _:NewLambda => throw new AssertionError(s"Invalid tree: $tree") } @@ -1039,6 +1040,13 @@ private class FunctionEmitter private ( * not need to store the receiver in a local at all. * For the case with the args, it does not hurt either way. We could * move it out, but that would make for a less consistent codegen. + * + * Loading the arguments and storing them in locals inside the block + * only works if their type is defaultable. Currently, for instance + * methods, parameter types are always defaultable, so this is fine. + * We may need to revisit this strategy if that invariant changes. + * If we do, it may be better to use different code paths for the + * no-args case and the with-args case. See #5165 for more context. */ val argsLocals = fb.block(watpe.RefType.any) { labelNotOurObject => // Load receiver and arguments and store them in temporary variables @@ -1637,6 +1645,55 @@ private class FunctionEmitter private ( case Throw => fb += wa.ExternConvertAny fb += wa.Throw(genTagID.exception) + + // Floating point bit manipulation + case Float_toBits => + val bitsLocal = addSyntheticLocal(watpe.Int32) + // bits := toRawBits(arg) + fb += wa.I32ReinterpretF32 + fb += wa.LocalTee(bitsLocal) + // if ((bits & ~SignBit) > bit pattern of Infinity) + fb += wa.I32Const(~Int.MinValue) + fb += wa.I32And + fb += wa.I32Const(java.lang.Float.floatToIntBits(Float.PositiveInfinity)) + fb += wa.I32GtU + fb.ifThen() { // there is a good chance that this branch is predictably false, so don't use wa.Select + // then it's NaN; replace with the canonical bit pattern + fb += wa.I32Const(java.lang.Float.floatToIntBits(Float.NaN)) + fb += wa.LocalSet(bitsLocal) + } + // result is in bits + fb += wa.LocalGet(bitsLocal) + case Float_fromBits => + fb += wa.F32ReinterpretI32 + case Double_toBits => + val bitsLocal = addSyntheticLocal(watpe.Int64) + // bits := toRawBits(arg) + fb += wa.I64ReinterpretF64 + fb += wa.LocalTee(bitsLocal) + // if ((bits & ~SignBit) > bit pattern of Infinity) + fb += wa.I64Const(~Long.MinValue) + fb += wa.I64And + fb += wa.I64Const(java.lang.Double.doubleToLongBits(Double.PositiveInfinity)) + fb += wa.I64GtU + fb.ifThen() { // there is a good chance that this branch is predictably false, so don't use wa.Select + // then it's NaN; replace with the canonical bit pattern + fb += wa.I64Const(java.lang.Double.doubleToLongBits(Double.NaN)) + fb += wa.LocalSet(bitsLocal) + } + // result is in bits + fb += wa.LocalGet(bitsLocal) + case Double_fromBits => + fb += wa.F64ReinterpretI64 + + case Int_clz => + fb += wa.I32Clz + case Long_clz => + fb += wa.I64Clz + fb += wa.I32WrapI64 + + case UnsignedIntToLong => + fb += wa.I64ExtendI32U } tree.tpe @@ -1686,33 +1743,34 @@ private class FunctionEmitter private ( case String_+ => genStringConcat(tree) - case Int_/ => - rhs match { - case IntLiteral(rhsValue) => - genDivModByConstant(tree, isDiv = true, rhsValue, wa.I32Const(_), wa.I32Sub, wa.I32DivS) - case _ => - genDivMod(tree, isDiv = true, wa.I32Const(_), wa.I32Eqz, wa.I32Eq, wa.I32Sub, wa.I32DivS) + case Int_/ | Int_% | Int_unsigned_/ | Int_unsigned_% => + val isSignedDiv = op == Int_/ + val mainOp = (op: @switch) match { + case Int_/ => wa.I32DivS + case Int_% => wa.I32RemS + case Int_unsigned_/ => wa.I32DivU + case Int_unsigned_% => wa.I32RemU } - case Int_% => rhs match { case IntLiteral(rhsValue) => - genDivModByConstant(tree, isDiv = false, rhsValue, wa.I32Const(_), wa.I32Sub, wa.I32RemS) + genDivModByConstant(tree, isSignedDiv, rhsValue, wa.I32Const(_), wa.I32Sub, mainOp) case _ => - genDivMod(tree, isDiv = false, wa.I32Const(_), wa.I32Eqz, wa.I32Eq, wa.I32Sub, wa.I32RemS) + genDivMod(tree, isSignedDiv, wa.I32Const(_), wa.I32Eqz, wa.I32Eq, wa.I32Sub, mainOp) } - case Long_/ => - rhs match { - case LongLiteral(rhsValue) => - genDivModByConstant(tree, isDiv = true, rhsValue, wa.I64Const(_), wa.I64Sub, wa.I64DivS) - case _ => - genDivMod(tree, isDiv = true, wa.I64Const(_), wa.I64Eqz, wa.I64Eq, wa.I64Sub, wa.I64DivS) + + case Long_/ | Long_% | Long_unsigned_/ | Long_unsigned_% => + val isSignedDiv = op == Long_/ + val mainOp = (op: @switch) match { + case Long_/ => wa.I64DivS + case Long_% => wa.I64RemS + case Long_unsigned_/ => wa.I64DivU + case Long_unsigned_% => wa.I64RemU } - case Long_% => rhs match { case LongLiteral(rhsValue) => - genDivModByConstant(tree, isDiv = false, rhsValue, wa.I64Const(_), wa.I64Sub, wa.I64RemS) + genDivModByConstant(tree, isSignedDiv, rhsValue, wa.I64Const(_), wa.I64Sub, mainOp) case _ => - genDivMod(tree, isDiv = false, wa.I64Const(_), wa.I64Eqz, wa.I64Eq, wa.I64Sub, wa.I64RemS) + genDivMod(tree, isSignedDiv, wa.I64Const(_), wa.I64Eqz, wa.I64Eq, wa.I64Sub, mainOp) } case Long_<< => @@ -1919,6 +1977,16 @@ private class FunctionEmitter private ( case Double_>= => wa.F64Ge case Class_newArray => wa.Call(genFunctionID.newArray) + + case Int_unsigned_< => wa.I32LtU + case Int_unsigned_<= => wa.I32LeU + case Int_unsigned_> => wa.I32GtU + case Int_unsigned_>= => wa.I32GeU + + case Long_unsigned_< => wa.I64LtU + case Long_unsigned_<= => wa.I64LeU + case Long_unsigned_> => wa.I64GtU + case Long_unsigned_>= => wa.I64GeU } } @@ -2088,7 +2156,7 @@ private class FunctionEmitter private ( } } - private def genDivModByConstant[T](tree: BinaryOp, isDiv: Boolean, + private def genDivModByConstant[T](tree: BinaryOp, isSignedDiv: Boolean, rhsValue: T, const: T => wa.Instr, sub: wa.Instr, mainOp: wa.Instr)( implicit num: Numeric[T]): Type = { /* When we statically know the value of the rhs, we can avoid the @@ -2098,8 +2166,7 @@ private class FunctionEmitter private ( import BinaryOp._ - val BinaryOp(op, lhs, rhs) = tree - assert(op == Int_/ || op == Int_% || op == Long_/ || op == Long_%) + val BinaryOp(_, lhs, rhs) = tree val tpe = tree.tpe @@ -2108,7 +2175,7 @@ private class FunctionEmitter private ( markPosition(tree) genThrowArithmeticException()(tree.pos) NothingType - } else if (isDiv && rhsValue == num.fromInt(-1)) { + } else if (isSignedDiv && rhsValue == num.fromInt(-1)) { /* MinValue / -1 overflows; it traps in Wasm but we need to wrap. * We rewrite as `0 - lhs` so that we do not need any test. */ @@ -2128,7 +2195,7 @@ private class FunctionEmitter private ( } } - private def genDivMod[T](tree: BinaryOp, isDiv: Boolean, const: T => wa.Instr, + private def genDivMod[T](tree: BinaryOp, isSignedDiv: Boolean, const: T => wa.Instr, eqz: wa.Instr, eqInstr: wa.Instr, sub: wa.Instr, mainOp: wa.Instr)( implicit num: Numeric[T]): Type = { /* Here we perform the same steps as in the static case, but using @@ -2137,8 +2204,7 @@ private class FunctionEmitter private ( import BinaryOp._ - val BinaryOp(op, lhs, rhs) = tree - assert(op == Int_/ || op == Int_% || op == Long_/ || op == Long_%) + val BinaryOp(_, lhs, rhs) = tree val tpe = tree.tpe.asInstanceOf[PrimType] val wasmType = transformPrimType(tpe) @@ -2156,7 +2222,7 @@ private class FunctionEmitter private ( fb.ifThen() { genThrowArithmeticException()(tree.pos) } - if (isDiv) { + if (isSignedDiv) { // Handle the MinValue / -1 corner case fb += wa.LocalGet(rhsLocal) fb += const(num.fromInt(-1)) @@ -2173,7 +2239,7 @@ private class FunctionEmitter private ( fb += mainOp } } else { - // lhs % rhs + // lhs mainOp rhs fb += wa.LocalGet(lhsLocal) fb += wa.LocalGet(rhsLocal) fb += mainOp @@ -3622,6 +3688,11 @@ private class FunctionEmitter private ( * we cannot use the stack for the `try_table` itself: each label has a * dedicated local for its result if it comes from such a crossing `return`. * + * Those locals must have defaultable types, because they are read outside of + * the block where they are first ininitialized. If their natural type is not + * defaultable, we make it defaultable, and cast away nullability when we + * read them back. See #5165. + * * Two more complications: * * - If the `finally` block itself contains another `try..finally`, they may @@ -3849,7 +3920,7 @@ private class FunctionEmitter private ( _crossInfo.getOrElse { val destinationTag = allocateDestinationTag() val resultTypes = transformResultType(expectedType) - val resultLocals = resultTypes.map(addSyntheticLocal(_)) + val resultLocals = resultTypes.map(tpe => addSyntheticLocal(tpe.toDefaultableType)) val crossLabel = fb.genLabel() val info = CrossInfo(destinationTag, resultLocals, crossLabel) _crossInfo = Some(info) @@ -3940,8 +4011,11 @@ private class FunctionEmitter private ( // Add the `br`, `end` and `local.get` at the current position, as usual fb += wa.Br(entry.regularWasmLabel) fb += wa.End - for (local <- resultLocals) + for ((local, origType) <- resultLocals.zip(ty)) { fb += wa.LocalGet(local) + if (!origType.isDefaultable) + fb += wa.RefAsNonNull + } } fb += wa.End @@ -3958,7 +4032,7 @@ private class FunctionEmitter private ( val entry = new TryFinallyEntry(currentUnwindingStackDepth) val resultType = transformResultType(expectedType) - val resultLocals = resultType.map(addSyntheticLocal(_)) + val resultLocals = resultType.map(tpe => addSyntheticLocal(tpe.toDefaultableType)) markPosition(tree) @@ -4073,8 +4147,11 @@ private class FunctionEmitter private ( } // end block $done // reload the result onto the stack - for (resultLocal <- resultLocals) + for ((resultLocal, origType) <- resultLocals.zip(resultType)) { fb += wa.LocalGet(resultLocal) + if (!origType.isDefaultable) + fb += wa.RefAsNonNull + } if (expectedType == NothingType) fb += wa.Unreachable diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmTransients.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmTransients.scala index bf41838a98..202e1e2ee4 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmTransients.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmTransients.scala @@ -47,11 +47,9 @@ object WasmTransients { Transient(WasmUnaryOp(op, transformer.transform(lhs))) def wasmInstr: wa.SimpleInstr = (op: @switch) match { - case I32Clz => wa.I32Clz case I32Ctz => wa.I32Ctz case I32Popcnt => wa.I32Popcnt - case I64Clz => wa.I64Clz case I64Ctz => wa.I64Ctz case I64Popcnt => wa.I64Popcnt @@ -62,11 +60,6 @@ object WasmTransients { case F64Floor => wa.F64Floor case F64Nearest => wa.F64Nearest case F64Sqrt => wa.F64Sqrt - - case I32ReinterpretF32 => wa.I32ReinterpretF32 - case I64ReinterpretF64 => wa.I64ReinterpretF64 - case F32ReinterpretI32 => wa.F32ReinterpretI32 - case F64ReinterpretI64 => wa.F64ReinterpretI64 } def printIR(out: IRTreePrinter): Unit = { @@ -80,38 +73,31 @@ object WasmTransients { /** Codes are raw Ints to be able to write switch matches on them. */ type Code = Int - final val I32Clz = 1 - final val I32Ctz = 2 - final val I32Popcnt = 3 - - final val I64Clz = 4 - final val I64Ctz = 5 - final val I64Popcnt = 6 + final val I32Ctz = 1 + final val I32Popcnt = 2 - final val F32Abs = 7 + final val I64Ctz = 3 + final val I64Popcnt = 4 - final val F64Abs = 8 - final val F64Ceil = 9 - final val F64Floor = 10 - final val F64Nearest = 11 - final val F64Sqrt = 12 + final val F32Abs = 5 - final val I32ReinterpretF32 = 13 - final val I64ReinterpretF64 = 14 - final val F32ReinterpretI32 = 15 - final val F64ReinterpretI64 = 16 + final val F64Abs = 6 + final val F64Ceil = 7 + final val F64Floor = 8 + final val F64Nearest = 9 + final val F64Sqrt = 10 def resultTypeOf(op: Code): Type = (op: @switch) match { - case I32Clz | I32Ctz | I32Popcnt | I32ReinterpretF32 => + case I32Ctz | I32Popcnt => IntType - case I64Clz | I64Ctz | I64Popcnt | I64ReinterpretF64 => + case I64Ctz | I64Popcnt => LongType - case F32Abs | F32ReinterpretI32 => + case F32Abs => FloatType - case F64Abs | F64Ceil | F64Floor | F64Nearest | F64Sqrt | F64ReinterpretI64 => + case F64Abs | F64Ceil | F64Floor | F64Nearest | F64Sqrt => DoubleType } } @@ -147,13 +133,9 @@ object WasmTransients { def wasmInstr: wa.SimpleInstr = (op: @switch) match { case I32GtU => wa.I32GtU - case I32DivU => wa.I32DivU - case I32RemU => wa.I32RemU case I32Rotl => wa.I32Rotl case I32Rotr => wa.I32Rotr - case I64DivU => wa.I64DivU - case I64RemU => wa.I64RemU case I64Rotl => wa.I64Rotl case I64Rotr => wa.I64Rotr @@ -177,13 +159,9 @@ object WasmTransients { final val I32GtU = 1 - final val I32DivU = 2 - final val I32RemU = 3 final val I32Rotl = 4 final val I32Rotr = 5 - final val I64DivU = 6 - final val I64RemU = 7 final val I64Rotl = 8 final val I64Rotr = 9 @@ -197,10 +175,10 @@ object WasmTransients { case I32GtU => BooleanType - case I32DivU | I32RemU | I32Rotl | I32Rotr => + case I32Rotl | I32Rotr => IntType - case I64DivU | I64RemU | I64Rotl | I64Rotr => + case I64Rotl | I64Rotr => LongType case F32Min | F32Max => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Types.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Types.scala index 58f07eba99..62b3be1849 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Types.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Types.scala @@ -33,7 +33,24 @@ object Types { * typing" point of view. It is also the kind of type we manipulate the most * across the backend, so it also makes sense for it to be the "default". */ - sealed abstract class Type extends StorageType + sealed abstract class Type extends StorageType { + /** Returns true if and only if this type is defaultable. */ + final def isDefaultable: Boolean = this match { + case RefType(nullable, _) => nullable + case _ => true + } + + /** Returns a defaultable supertype of this type. + * + * If this type is already defaultable, return `this`. Otherwise, this + * type must be a non-nullable reference type, and this method returns the + * nullable variant. + */ + final def toDefaultableType: Type = this match { + case RefType(false, heapType) => RefType.nullable(heapType) + case _ => this + } + } /** Convenience superclass for `Type`s that are encoded with a simple opcode. */ sealed abstract class SimpleType(val textName: String, val binaryCode: Byte) extends Type diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index a1c9f6363d..2d1437ee5f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -761,6 +761,13 @@ private final class ClassDefChecker(classDef: ClassDef, checkTree(thenp, env) checkTree(elsep, env) + case LinkTimeIf(cond, thenp, elsep) => + if (!featureSet.supports(FeatureSet.LinkTimeNodes)) + reportError(i"Illegal link-time if after desugaring") + checkLinkTimeTree(cond, BooleanType) + checkTree(thenp, env) + checkTree(elsep, env) + case While(cond, body) => checkTree(cond, env) checkTree(body, env) @@ -923,9 +930,16 @@ private final class ClassDefChecker(classDef: ClassDef, } case LinkTimeProperty(name) => - if (!featureSet.supports(FeatureSet.LinkTimeProperty)) + if (!featureSet.supports(FeatureSet.LinkTimeNodes)) reportError(i"Illegal link-time property '$name' after desugaring") + tree.tpe match { + case BooleanType | IntType | StringType => + () // ok + case tpe => + reportError(i"$tpe is not a valid type for LinkTimeProperty") + } + // JavaScript expressions case JSNew(ctor, args) => @@ -1091,6 +1105,60 @@ private final class ClassDefChecker(classDef: ClassDef, } } + private def checkLinkTimeTree(tree: Tree, expectedType: PrimType): Unit = { + implicit val ctx = ErrorContext(tree) + + /* For link-time trees, we need to check the types. Having a well-typed + * condition is required for `LinkTimeIf` to be resolved, and that happens + * before IR checking. Fortunately, only trivial primitive types can appear + * in link-time trees, and it is therefore possible to check them now. + */ + if (tree.tpe != expectedType) + reportError(i"$expectedType expected but ${tree.tpe} found in link-time tree") + + /* Unlike the evaluation algorithm, at this time we allow LinkTimeProperty's + * that are not actually available. We only check that their declared type + * matches the expected type. If it does not exist or does not have the + * type it was declared with, that constitutes a *linking error*, but it + * does not make the ClassDef invalid. + */ + + tree match { + case _:IntLiteral | _:BooleanLiteral | _:StringLiteral | _:LinkTimeProperty => + () // ok + + case UnaryOp(op, lhs) => + import UnaryOp._ + op match { + case Boolean_! => + checkLinkTimeTree(lhs, BooleanType) + case _ => + reportError(i"illegal unary op $op in link-time tree") + } + + case BinaryOp(op, lhs, rhs) => + import BinaryOp._ + op match { + case Boolean_== | Boolean_!= | Boolean_| | Boolean_& => + checkLinkTimeTree(lhs, BooleanType) + checkLinkTimeTree(rhs, BooleanType) + case Int_== | Int_!= | Int_< | Int_<= | Int_> | Int_>= => + checkLinkTimeTree(lhs, IntType) + checkLinkTimeTree(rhs, IntType) + case _ => + reportError(i"illegal binary op $op in link-time tree") + } + + case LinkTimeIf(cond, thenp, elsep) => + checkLinkTimeTree(cond, BooleanType) + checkLinkTimeTree(thenp, expectedType) + checkLinkTimeTree(elsep, expectedType) + + case _ => + reportError(i"illegal tree of class ${tree.getClass().getName()} in link-time tree") + } + } + private def checkArrayType(tpe: ArrayType)( implicit ctx: ErrorContext): Unit = { checkArrayTypeRef(tpe.arrayTypeRef) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/FeatureSet.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/FeatureSet.scala index 33cbeaa135..94aabffff1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/FeatureSet.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/FeatureSet.scala @@ -36,8 +36,8 @@ private[checker] object FeatureSet { // Individual features - /** The `LinkTimeProperty` IR node. */ - val LinkTimeProperty = new FeatureSet(1 << 0) + /** Link-time IR nodes: `LinkTimeProperty` and `LinkTimeIf`. */ + val LinkTimeNodes = new FeatureSet(1 << 0) /** The `NewLambda` IR node. */ val NewLambda = new FeatureSet(1 << 1) @@ -84,7 +84,7 @@ private[checker] object FeatureSet { /** Features that must be desugared away. */ private val NeedsDesugaring = - LinkTimeProperty | NewLambda + LinkTimeNodes | NewLambda /** IR that is only the result of desugaring (currently empty). */ private val Desugared = diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index b66dfeea1f..c25ae55672 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -24,13 +24,13 @@ import org.scalajs.ir.WellKnownNames._ import org.scalajs.logging._ -import org.scalajs.linker.frontend.LinkingUnit +import org.scalajs.linker.frontend.{LinkingUnit, LinkTimeEvaluator, LinkTimeProperties} import org.scalajs.linker.standard.LinkedClass import org.scalajs.linker.checker.ErrorReporter._ /** Checker for the validity of the IR. */ -private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, - previousPhase: CheckingPhase) { +private final class IRChecker(linkTimeProperties: LinkTimeProperties, + unit: LinkingUnit, reporter: ErrorReporter, previousPhase: CheckingPhase) { import IRChecker._ import reporter.reportError @@ -315,6 +315,26 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheckExpect(thenp, env, tpe) typecheckExpect(elsep, env, tpe) + case LinkTimeIf(cond, thenp, elsep) if featureSet.supports(FeatureSet.LinkTimeNodes) => + /* The `cond` is entirely checked in ClassDefChecker. + * + * We must only check the branch that is actually selected. + * We *cannot* check the dropped branch, because it may refer to types + * that are dropped by the reachability analysis (which is the whole + * point of LinkTimeIf). It is OK to have ill-typed IR in the dropped + * branch, because it is guaranteed to disappear during desugaring, + * before types are relied upon for any optimization or emission. + */ + LinkTimeEvaluator.tryEvalLinkTimeBooleanExpr(linkTimeProperties, cond) match { + case Some(value) => + if (value) + typecheckExpect(thenp, env, tree.tpe) + else + typecheckExpect(elsep, env, tree.tpe) + case None => + reportError(i"could not evaluate link-time condition: $cond") + } + case While(cond, body) => typecheckExpect(cond, env, BooleanType) typecheck(body, env) @@ -518,13 +538,15 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, ByteType case ShortToInt => ShortType - case IntToLong | IntToDouble | IntToChar | IntToByte | IntToShort => + case IntToLong | IntToDouble | IntToChar | IntToByte | IntToShort | + Float_fromBits | Int_clz | UnsignedIntToLong => IntType - case LongToInt | LongToDouble | LongToFloat => + case LongToInt | LongToDouble | LongToFloat | Double_fromBits | + Long_clz => LongType - case FloatToDouble => + case FloatToDouble | Float_toBits => FloatType - case DoubleToInt | DoubleToFloat | DoubleToLong => + case DoubleToInt | DoubleToFloat | DoubleToLong | Double_toBits => DoubleType case String_length => StringType @@ -551,11 +573,15 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, BooleanType case Int_+ | Int_- | Int_* | Int_/ | Int_% | Int_| | Int_& | Int_^ | Int_<< | Int_>>> | Int_>> | - Int_== | Int_!= | Int_< | Int_<= | Int_> | Int_>= => + Int_== | Int_!= | Int_< | Int_<= | Int_> | Int_>= | + Int_unsigned_/ | Int_unsigned_% | + Int_unsigned_< | Int_unsigned_<= | Int_unsigned_> | Int_unsigned_>= => IntType case Long_+ | Long_- | Long_* | Long_/ | Long_% | Long_| | Long_& | Long_^ | Long_<< | Long_>>> | Long_>> | - Long_== | Long_!= | Long_< | Long_<= | Long_> | Long_>= => + Long_== | Long_!= | Long_< | Long_<= | Long_> | Long_>= | + Long_unsigned_/ | Long_unsigned_% | + Long_unsigned_< | Long_unsigned_<= | Long_unsigned_> | Long_unsigned_>= => LongType case Float_+ | Float_- | Float_* | Float_/ | Float_% => FloatType @@ -609,7 +635,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheckAny(expr, env) checkIsAsInstanceTargetType(tpe) - case LinkTimeProperty(name) if featureSet.supports(FeatureSet.LinkTimeProperty) => + case LinkTimeProperty(name) if featureSet.supports(FeatureSet.LinkTimeNodes) => // JavaScript expressions @@ -793,7 +819,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, } case _:RecordSelect | _:RecordValue | _:Transient | - _:JSSuperConstructorCall | _:LinkTimeProperty | + _:JSSuperConstructorCall | _:LinkTimeProperty | _:LinkTimeIf | _:ApplyTypedClosure | _:NewLambda => reportError("invalid tree") } @@ -963,9 +989,10 @@ object IRChecker { * * @return Count of IR checking errors (0 in case of success) */ - def check(unit: LinkingUnit, logger: Logger, previousPhase: CheckingPhase): Int = { + def check(linkTimeProperties: LinkTimeProperties, unit: LinkingUnit, + logger: Logger, previousPhase: CheckingPhase): Int = { val reporter = new LoggerErrorReporter(logger) - new IRChecker(unit, reporter, previousPhase).check() + new IRChecker(linkTimeProperties, unit, reporter, previousPhase).check() reporter.errorCount } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala index 62d05ff87e..b88ea4fd55 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala @@ -35,6 +35,8 @@ import Analysis._ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { import BaseLinker._ + private val linkTimeProperties = LinkTimeProperties.fromCoreSpec(config.coreSpec) + private val irLoader = new FileIRLoader private val analyzer = { val checkIRFor = if (checkIR) Some(CheckingPhase.Compiler) else None @@ -58,7 +60,8 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { } yield { if (checkIR) { logger.time("Linker: Check IR") { - val errorCount = IRChecker.check(linkResult, logger, CheckingPhase.BaseLinker) + val errorCount = IRChecker.check(linkTimeProperties, linkResult, + logger, CheckingPhase.BaseLinker) if (errorCount != 0) { throw new LinkingException( s"There were $errorCount IR checking errors.") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Desugarer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Desugarer.scala index 44e2f66d09..b97423440d 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Desugarer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Desugarer.scala @@ -28,7 +28,9 @@ import org.scalajs.ir.{Position, Version} final class Desugarer(config: CommonPhaseConfig, checkIR: Boolean) { import Desugarer._ - private val desugarTransformer = new DesugarTransformer(config.coreSpec) + private val linkTimeProperties = LinkTimeProperties.fromCoreSpec(config.coreSpec) + + private val desugarTransformer = new DesugarTransformer(linkTimeProperties) def desugar(unit: LinkingUnit, logger: Logger): LinkingUnit = { val result = logger.time("Desugarer: Desugar") { @@ -41,7 +43,8 @@ final class Desugarer(config: CommonPhaseConfig, checkIR: Boolean) { if (checkIR) { logger.time("Desugarer: Check IR") { - val errorCount = IRChecker.check(result, logger, CheckingPhase.Desugarer) + val errorCount = IRChecker.check(linkTimeProperties, result, logger, + CheckingPhase.Desugarer) if (errorCount != 0) { throw new AssertionError( s"There were $errorCount IR checking errors after desugaring (this is a Scala.js bug)") @@ -118,7 +121,7 @@ final class Desugarer(config: CommonPhaseConfig, checkIR: Boolean) { private[linker] object Desugarer { - private final class DesugarTransformer(coreSpec: CoreSpec) + private final class DesugarTransformer(linkTimeProperties: LinkTimeProperties) extends ClassTransformer { /* Cache the names generated for lambda classes because computing their @@ -135,8 +138,32 @@ private[linker] object Desugarer { override def transform(tree: Tree): Tree = { tree match { - case prop: LinkTimeProperty => - coreSpec.linkTimeProperties.transformLinkTimeProperty(prop) + case LinkTimeProperty(name) => + implicit val pos = tree.pos + val value = linkTimeProperties.get(name).getOrElse { + throw new IllegalArgumentException( + s"link time property not found: '$name' of type ${tree.tpe}") + } + value match { + case LinkTimeProperties.LinkTimeBoolean(value) => BooleanLiteral(value) + case LinkTimeProperties.LinkTimeInt(value) => IntLiteral(value) + case LinkTimeProperties.LinkTimeString(value) => StringLiteral(value) + } + + case LinkTimeIf(cond, thenp, elsep) => + LinkTimeEvaluator.tryEvalLinkTimeBooleanExpr(linkTimeProperties, cond) match { + case Some(result) => + if (result) + transform(thenp) + else + transform(elsep) + case None => + throw new AssertionError( + s"Invalid link-time condition should not have passed the reachability analysis:\n" + + s"${tree.show}\n" + + s"at ${tree.pos}.\n" + + "Consider running the linker with `withCheckIR(true)` before submitting a bug report.") + } case NewLambda(descriptor, fun) => implicit val pos = tree.pos diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkTimeEvaluator.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkTimeEvaluator.scala new file mode 100644 index 0000000000..3ab224306f --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkTimeEvaluator.scala @@ -0,0 +1,129 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker.frontend + +import org.scalajs.ir.Position +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Trees.LinkTimeProperty._ + +import org.scalajs.linker.frontend.LinkTimeProperties._ +import org.scalajs.linker.interface.LinkingException + +private[linker] object LinkTimeEvaluator { + + /** Try and evaluate a link-time expression tree as a boolean value. + * + * This method assumes that the given `tree` is valid according to the + * `ClassDefChecker` and that its `tpe` is `BooleanType`. + * If that is not the case, it may throw or return an arbitrary result. + * + * Returns `None` if any subtree that needed evaluation was a missing + * `LinkTimeProperty` or one with the wrong type (i.e., one that would not + * pass the reachability analysis). + */ + def tryEvalLinkTimeBooleanExpr( + linkTimeProperties: LinkTimeProperties, tree: Tree): Option[Boolean] = { + implicit val pos = tree.pos + + tryEvalLinkTimeExpr(linkTimeProperties, tree).map(booleanValue(_)) + } + + /** Try and evaluate a link-time expression tree. + * + * This method assumes that the given `tree` is valid according to the + * `ClassDefChecker`. + * If that is not the case, it may throw or return an arbitrary result. + * + * Returns `None` if any subtree that needed evaluation was a missing + * `LinkTimeProperty` or one with the wrong type (i.e., one that would not + * pass the reachability analysis). + */ + private def tryEvalLinkTimeExpr( + props: LinkTimeProperties, tree: Tree): Option[LinkTimeValue] = { + implicit val pos = tree.pos + + tree match { + case IntLiteral(value) => Some(LinkTimeInt(value)) + case BooleanLiteral(value) => Some(LinkTimeBoolean(value)) + case StringLiteral(value) => Some(LinkTimeString(value)) + + case LinkTimeProperty(name) => + props.get(name).filter(_.tpe == tree.tpe) + + case UnaryOp(op, lhs) => + import UnaryOp._ + for { + l <- tryEvalLinkTimeExpr(props, lhs) + } yield { + op match { + case Boolean_! => LinkTimeBoolean(!booleanValue(l)) + + case _ => + throw new LinkingException( + s"Illegal unary op $op in link-time tree at $pos") + } + } + + case BinaryOp(op, lhs, rhs) => + import BinaryOp._ + for { + l <- tryEvalLinkTimeExpr(props, lhs) + r <- tryEvalLinkTimeExpr(props, rhs) + } yield { + op match { + case Boolean_== => LinkTimeBoolean(booleanValue(l) == booleanValue(r)) + case Boolean_!= => LinkTimeBoolean(booleanValue(l) != booleanValue(r)) + case Boolean_| => LinkTimeBoolean(booleanValue(l) | booleanValue(r)) + case Boolean_& => LinkTimeBoolean(booleanValue(l) & booleanValue(r)) + + case Int_== => LinkTimeBoolean(intValue(l) == intValue(r)) + case Int_!= => LinkTimeBoolean(intValue(l) != intValue(r)) + case Int_< => LinkTimeBoolean(intValue(l) < intValue(r)) + case Int_<= => LinkTimeBoolean(intValue(l) <= intValue(r)) + case Int_> => LinkTimeBoolean(intValue(l) > intValue(r)) + case Int_>= => LinkTimeBoolean(intValue(l) >= intValue(r)) + + case _ => + throw new LinkingException( + s"Illegal binary op $op in link-time tree at $pos") + } + } + + case LinkTimeIf(cond, thenp, elsep) => + tryEvalLinkTimeExpr(props, cond).flatMap { c => + if (booleanValue(c)) + tryEvalLinkTimeExpr(props, thenp) + else + tryEvalLinkTimeExpr(props, elsep) + } + + case _ => + throw new LinkingException( + s"Illegal tree of class ${tree.getClass().getName()} in link-time tree at $pos") + } + } + + private def intValue(value: LinkTimeValue)(implicit pos: Position): Int = value match { + case LinkTimeInt(value) => + value + case _ => + throw new LinkingException(s"Value of type int expected but got $value at $pos") + } + + private def booleanValue(value: LinkTimeValue)(implicit pos: Position): Boolean = value match { + case LinkTimeBoolean(value) => + value + case _ => + throw new LinkingException(s"Value of type boolean expected but got $value at $pos") + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkTimeProperties.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkTimeProperties.scala similarity index 50% rename from linker/shared/src/main/scala/org/scalajs/linker/standard/LinkTimeProperties.scala rename to linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkTimeProperties.scala index 875196c736..d2c12c67d0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkTimeProperties.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkTimeProperties.scala @@ -10,15 +10,16 @@ * additional information regarding copyright ownership. */ -package org.scalajs.linker.standard +package org.scalajs.linker.frontend -import org.scalajs.ir.{Types => jstpe, Trees => js} import org.scalajs.ir.Trees.LinkTimeProperty._ +import org.scalajs.ir.Types._ import org.scalajs.ir.ScalaJSVersions -import org.scalajs.ir.Position.NoPosition -import org.scalajs.linker.interface.{Semantics, ESFeatures} -private[linker] final class LinkTimeProperties ( +import org.scalajs.linker.interface.{ESVersion => _, _} +import org.scalajs.linker.standard.CoreSpec + +final class LinkTimeProperties private ( semantics: Semantics, esFeatures: ESFeatures, targetIsWebAssembly: Boolean @@ -38,31 +39,24 @@ private[linker] final class LinkTimeProperties ( LinkTimeString(ScalaJSVersions.current) ) - def validate(name: String, tpe: jstpe.Type): Boolean = { - linkTimeProperties.get(name).exists { - case _: LinkTimeBoolean => tpe == jstpe.BooleanType - case _: LinkTimeInt => tpe == jstpe.IntType - case _: LinkTimeString => tpe == jstpe.StringType - } - } + def get(name: String): Option[LinkTimeValue] = + linkTimeProperties.get(name) +} + +object LinkTimeProperties { + sealed abstract class LinkTimeValue(val tpe: Type) - def transformLinkTimeProperty(prop: js.LinkTimeProperty): js.Literal = { - val value = linkTimeProperties.getOrElse(prop.name, - throw new IllegalArgumentException(s"link time property not found: '${prop.name}' of type ${prop.tpe}")) - value match { - case LinkTimeBoolean(value) => - js.BooleanLiteral(value)(prop.pos) - case LinkTimeInt(value) => - js.IntLiteral(value)(prop.pos) - case LinkTimeString(value) => - js.StringLiteral(value)(prop.pos) - } + final case class LinkTimeInt(value: Int) extends LinkTimeValue(IntType) + + final case class LinkTimeBoolean(value: Boolean) extends LinkTimeValue(BooleanType) + + final case class LinkTimeString(value: String) extends LinkTimeValue(StringType) { + // Being extra careful + require(value != null, "LinkTimeString requires a non-null value.") } -} -private[linker] object LinkTimeProperties { - sealed abstract class LinkTimeValue - final case class LinkTimeInt(value: Int) extends LinkTimeValue - final case class LinkTimeBoolean(value: Boolean) extends LinkTimeValue - final case class LinkTimeString(value: String) extends LinkTimeValue + def fromCoreSpec(coreSpec: CoreSpec): LinkTimeProperties = { + new LinkTimeProperties(coreSpec.semantics, coreSpec.esFeatures, + coreSpec.targetIsWebAssembly) + } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala index 0f074adf55..4f778351ba 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala @@ -30,6 +30,8 @@ import org.scalajs.linker.analyzer._ final class Refiner(config: CommonPhaseConfig, checkIR: Boolean) { import Refiner._ + private val linkTimeProperties = LinkTimeProperties.fromCoreSpec(config.coreSpec) + private val irLoader = new ClassDefIRLoader private val analyzer = { val checkIRFor = if (checkIR) Some(CheckingPhase.Optimizer) else None @@ -81,7 +83,8 @@ final class Refiner(config: CommonPhaseConfig, checkIR: Boolean) { if (shouldRunIRChecker) { logger.time("Refiner: Check IR") { - val errorCount = IRChecker.check(result, logger, CheckingPhase.Optimizer) + val errorCount = IRChecker.check(linkTimeProperties, result, logger, + CheckingPhase.Optimizer) if (errorCount != 0) { throw new AssertionError( s"There were $errorCount IR checking errors after optimization (this is a Scala.js bug)") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 38bdb804c3..f9cd1e2c00 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -75,7 +75,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: multiple( cond(!targetIsWebAssembly && !esFeatures.allowBigIntsForLongs) { // Required by the intrinsics manipulating Longs - callMethods(LongImpl.RuntimeLongClass, LongImpl.AllIntrinsicMethods.toList) + callStaticMethods(LongImpl.RuntimeLongClass, LongImpl.AllIntrinsicMethods.toList) }, cond(targetIsWebAssembly) { // Required by the intrinsic CharacterCodePointToString diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 51cebcdcca..16ac1a4122 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -271,6 +271,8 @@ private[optimizer] abstract class OptimizerCore( (lhs, rhs) match { case (LongType, ClassType(LongImpl.RuntimeLongClass, _)) => true + case (ClassType(LongImpl.RuntimeLongClass, false), LongType) => + true case (ClassType(BoxedLongClass, lhsNullable), ClassType(LongImpl.RuntimeLongClass, rhsNullable)) => rhsNullable || !lhsNullable @@ -689,7 +691,8 @@ private[optimizer] abstract class OptimizerCore( _:JSGlobalRef | _:JSTypeOfGlobalRef | _:Literal => tree - case _:LinkTimeProperty | _:NewLambda | _:RecordSelect | _:Transient => + case _:LinkTimeProperty | _:LinkTimeIf | _:NewLambda | _:RecordSelect | + _:Transient => throw new IllegalArgumentException( s"Invalid tree in transform of class ${tree.getClass.getName}: $tree") } @@ -1504,14 +1507,14 @@ private[optimizer] abstract class OptimizerCore( BinaryOp(op, finishTransformExpr(lhs), finishTransformExpr(rhs)) (op: @switch) match { - case Int_/ | Int_% => + case Int_/ | Int_% | Int_unsigned_/ | Int_unsigned_% => rhs match { case PreTransLit(IntLiteral(r)) if r != 0 => finishNoSideEffects case _ => Block(newLhs, BinaryOp(op, IntLiteral(0), finishTransformExpr(rhs))) } - case Long_/ | Long_% => + case Long_/ | Long_% | Long_unsigned_/ | Long_unsigned_% => rhs match { case PreTransLit(LongLiteral(r)) if r != 0L => finishNoSideEffects @@ -1648,6 +1651,9 @@ private[optimizer] abstract class OptimizerCore( else Block(exprSideEffects, Transient(Cast(Null(), tpe))) + case Transient(GetFPBitsDataView) => + Skip()(stat.pos) + case _ => stat } @@ -1834,8 +1840,9 @@ private[optimizer] abstract class OptimizerCore( case NotFoundPureSoFar => rec(rhs).mapOrKeepGoingIf(BinaryOp(op, lhs, _)) { (op: @switch) match { - case Int_/ | Int_% | Long_/ | Long_% | String_+ | String_charAt | - Class_cast | Class_newArray => + case Int_/ | Int_% | Int_unsigned_/ | Int_unsigned_% | + Long_/ | Long_% | Long_unsigned_/ | Long_unsigned_% | + String_+ | String_charAt | Class_cast | Class_newArray => false case _ => true @@ -1900,6 +1907,8 @@ private[optimizer] abstract class OptimizerCore( case _: Literal => NotFoundPureSoFar + case Transient(GetFPBitsDataView) => + NotFoundPureSoFar case Closure(flags, captureParams, params, restParam, resultType, body, captureValues) => recs(captureValues).mapOrKeepGoing { newCaptureValues => @@ -2213,18 +2222,28 @@ private[optimizer] abstract class OptimizerCore( usePreTransform: Boolean)( cont: PreTransCont)( implicit scope: Scope): TailRec[Tree] = { - val ApplyStatic(flags, className, - methodIdent @ MethodIdent(methodName), args) = tree + val ApplyStatic(flags, className, methodIdent, args) = tree implicit val pos = tree.pos - val target = staticCall(className, MemberNamespace.forStaticCall(flags), - methodName) pretransformExprs(args) { targs => - pretransformSingleDispatch(flags, target, None, targs, isStat, usePreTransform)(cont) { - val newArgs = targs.map(finishTransformExpr) - cont(PreTransTree(ApplyStatic(flags, className, methodIdent, - newArgs)(tree.tpe))) - } + pretransformApplyStatic(flags, className, methodIdent, targs, tree.tpe, + isStat, usePreTransform)( + cont) + } + } + + private def pretransformApplyStatic(flags: ApplyFlags, className: ClassName, + methodIdent: MethodIdent, targs: List[PreTransform], resultType: Type, + isStat: Boolean, usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope, pos: Position): TailRec[Tree] = { + + val target = staticCall(className, MemberNamespace.forStaticCall(flags), + methodIdent.name) + pretransformSingleDispatch(flags, target, None, targs, isStat, usePreTransform)(cont) { + val newArgs = targs.map(finishTransformExpr) + cont(PreTransTree( + ApplyStatic(flags, className, methodIdent, newArgs)(resultType))) } } @@ -2728,28 +2747,6 @@ private[optimizer] abstract class OptimizerCore( def wasmBinaryOp(op: WasmBinaryOp.Code, lhs: PreTransform, rhs: PreTransform): Tree = Transient(WasmBinaryOp(op, finishTransformExpr(lhs), finishTransformExpr(rhs))) - def genericWasmDivModUnsigned(wasmOp: WasmBinaryOp.Code, signedOp: BinaryOp.Code, - equalsOp: BinaryOp.Code, zeroLiteral: Literal): TailRec[Tree] = { - targs(1) match { - case PreTransLit(IntLiteral(r)) if r != 0 => - contTree(wasmBinaryOp(wasmOp, targs(0), targs(1))) - case PreTransLit(LongLiteral(r)) if r != 0L => - contTree(wasmBinaryOp(wasmOp, targs(0), targs(1))) - case _ => - withNewTempLocalDefs(targs) { (localDefs, cont1) => - val List(lhsLocalDef, rhsLocalDef) = localDefs - cont1 { - If(BinaryOp(equalsOp, rhsLocalDef.newReplacement, zeroLiteral), { - // trigger the appropriate ArithmeticException - BinaryOp(signedOp, zeroLiteral, zeroLiteral) - }, { - wasmBinaryOp(wasmOp, lhsLocalDef.toPreTransform, rhsLocalDef.toPreTransform) - })(zeroLiteral.tpe).toPreTransform - } - } (cont) - } - } - (intrinsicCode: @switch) match { // Not an intrisic @@ -2846,17 +2843,6 @@ private[optimizer] abstract class OptimizerCore( // java.lang.Integer - case IntegerNLZ => - val tvalue = targs.head - tvalue match { - case PreTransLit(IntLiteral(value)) => - contTree(IntLiteral(Integer.numberOfLeadingZeros(value))) - case _ => - if (isWasm) - contTree(wasmUnaryOp(WasmUnaryOp.I32Clz, tvalue)) - else - default - } case IntegerNTZ => val tvalue = targs.head tvalue match { @@ -2891,23 +2877,8 @@ private[optimizer] abstract class OptimizerCore( contTree(wasmBinaryOp(WasmBinaryOp.I32Rotr, tvalue, tdistance)) } - case IntegerDivideUnsigned => - genericWasmDivModUnsigned(WasmBinaryOp.I32DivU, BinaryOp.Int_/, - BinaryOp.Int_==, IntLiteral(0)) - case IntegerRemainderUnsigned => - genericWasmDivModUnsigned(WasmBinaryOp.I32RemU, BinaryOp.Int_%, - BinaryOp.Int_==, IntLiteral(0)) - // java.lang.Long - case LongNLZ => - val tvalue = targs.head - tvalue match { - case PreTransLit(LongLiteral(value)) => - contTree(IntLiteral(java.lang.Long.numberOfLeadingZeros(value))) - case _ => - contTree(longToInt(wasmUnaryOp(WasmUnaryOp.I64Clz, tvalue))) - } case LongNTZ => val tvalue = targs.head tvalue match { @@ -2945,74 +2916,16 @@ private[optimizer] abstract class OptimizerCore( } case LongToString => - pretransformApply(ApplyFlags.empty, targs.head, - MethodIdent(LongImpl.toString_), Nil, StringClassType, + pretransformApplyStatic(ApplyFlags.empty, LongImpl.RuntimeLongClass, + MethodIdent(LongImpl.toString_), targs, StringClassType, isStat, usePreTransform)( cont) case LongCompare => - pretransformApply(ApplyFlags.empty, targs.head, - MethodIdent(LongImpl.compareToRTLong), targs.tail, IntType, + pretransformApplyStatic(ApplyFlags.empty, LongImpl.RuntimeLongClass, + MethodIdent(LongImpl.compare), targs, IntType, isStat, usePreTransform)( cont) - case LongDivideUnsigned => - if (isWasm) { - genericWasmDivModUnsigned(WasmBinaryOp.I64DivU, BinaryOp.Long_/, - BinaryOp.Long_==, LongLiteral(0L)) - } else { - pretransformApply(ApplyFlags.empty, targs.head, - MethodIdent(LongImpl.divideUnsigned), targs.tail, - ClassType(LongImpl.RuntimeLongClass, nullable = true), isStat, - usePreTransform)( - cont) - } - case LongRemainderUnsigned => - if (isWasm) { - genericWasmDivModUnsigned(WasmBinaryOp.I64RemU, BinaryOp.Long_%, - BinaryOp.Long_==, LongLiteral(0L)) - } else { - pretransformApply(ApplyFlags.empty, targs.head, - MethodIdent(LongImpl.remainderUnsigned), targs.tail, - ClassType(LongImpl.RuntimeLongClass, nullable = true), isStat, - usePreTransform)( - cont) - } - - // java.lang.Float - - case FloatToIntBits => - // The Wasm I32ReinterpretF32 is the *raw* version; we need to normalize NaNs - withNewTempLocalDefs(targs) { (localDefs, cont1) => - val argLocalDef = localDefs.head - def argToDouble = UnaryOp(UnaryOp.FloatToDouble, argLocalDef.newReplacement) - cont1 { - If(BinaryOp(BinaryOp.Double_!=, argToDouble, argToDouble), - IntLiteral(java.lang.Float.floatToIntBits(Float.NaN)), - wasmUnaryOp(WasmUnaryOp.I32ReinterpretF32, argLocalDef.toPreTransform))( - IntType).toPreTransform - } - } (cont) - - case IntBitsToFloat => - contTree(wasmUnaryOp(WasmUnaryOp.F32ReinterpretI32, targs.head)) - - // java.lang.Double - - case DoubleToLongBits => - // The Wasm I64ReinterpretF64 is the *raw* version; we need to normalize NaNs - withNewTempLocalDefs(targs) { (localDefs, cont1) => - val argLocalDef = localDefs.head - cont1 { - If(BinaryOp(BinaryOp.Double_!=, argLocalDef.newReplacement, argLocalDef.newReplacement), - LongLiteral(java.lang.Double.doubleToLongBits(Double.NaN)), - wasmUnaryOp(WasmUnaryOp.I64ReinterpretF64, argLocalDef.toPreTransform))( - LongType).toPreTransform - } - } (cont) - - case LongBitsToDouble => - contTree(wasmUnaryOp(WasmUnaryOp.F64ReinterpretI64, targs.head)) - // java.lang.Character case CharacterCodePointToString => @@ -3073,6 +2986,28 @@ private[optimizer] abstract class OptimizerCore( case MathMaxDouble => contTree(wasmBinaryOp(WasmBinaryOp.F64Max, targs.head, targs.tail.head)) + case MathMultiplyFull => + def expand(targs: List[PreTransform]): TailRec[Tree] = { + pretransformApplyStatic(ApplyFlags.empty, + LongImpl.RuntimeLongClass, + MethodIdent(LongImpl.multiplyFull), + targs, + ClassType(LongImpl.RuntimeLongClass, nullable = true), + isStat, usePreTransform)( + cont) + } + + targs match { + case List(PreTransLit(IntLiteral(x)), PreTransLit(IntLiteral(y))) => + // cannot actually call multiplyHigh to constant-fold because it is JDK9+ + contTree(LongLiteral(x.toLong * y.toLong)) + case List(tlhs, trhs @ PreTransLit(_)) => + // normalize a single constant on the left; the implementation is optimized for that case + expand(trhs :: tlhs :: Nil) + case _ => + expand(targs) + } + // scala.collection.mutable.ArrayBuilder case GenericArrayBuilderResult => @@ -3569,11 +3504,15 @@ private[optimizer] abstract class OptimizerCore( withBinding(rtLongBinding) { (scope1, cont1) => implicit val scope = scope1 val tRef = VarRef(tName)(rtLongClassType) - val newTree = New(LongImpl.RuntimeLongClass, - MethodIdent(LongImpl.initFromParts), - List(Apply(ApplyFlags.empty, tRef, MethodIdent(LongImpl.lo), Nil)(IntType), - Apply(ApplyFlags.empty, tRef, MethodIdent(LongImpl.hi), Nil)(IntType))) - pretransformExpr(newTree)(cont1) + + val lo = Apply(ApplyFlags.empty, tRef, MethodIdent(LongImpl.lo), Nil)(IntType) + val hi = Apply(ApplyFlags.empty, tRef, MethodIdent(LongImpl.hi), Nil)(IntType) + + pretransformExprs(lo, hi) { (tlo, thi) => + inlineClassConstructor(AllocationSite.Anonymous, LongImpl.RuntimeLongClass, + inlinedRTLongStructure, MethodIdent(LongImpl.initFromParts), List(tlo, thi), + () => throw new AssertionError(s"rolled-back RuntimeLong inlining at $pos"))(cont1) + } } (cont) } @@ -3581,33 +3520,11 @@ private[optimizer] abstract class OptimizerCore( implicit scope: Scope): TailRec[Tree] = { implicit val pos = pretrans.pos - // unfortunately nullable for the result types of methods - def rtLongClassType = ClassType(LongImpl.RuntimeLongClass, nullable = true) - - def expandLongModuleOp(methodName: MethodName, - arg: PreTransform): TailRec[Tree] = { - import LongImpl.{RuntimeLongModuleClass => modCls} - val receiver = - makeCast(LoadModule(modCls), ClassType(modCls, nullable = false)).toPreTransform - pretransformApply(ApplyFlags.empty, receiver, MethodIdent(methodName), - arg :: Nil, rtLongClassType, isStat = false, - usePreTransform = true)( - cont) - } - - def expandUnaryOp(methodName: MethodName, arg: PreTransform, - resultType: Type = rtLongClassType): TailRec[Tree] = { - pretransformApply(ApplyFlags.empty, arg, MethodIdent(methodName), Nil, - resultType, isStat = false, usePreTransform = true)( - cont) - } - - def expandBinaryOp(methodName: MethodName, lhs: PreTransform, - rhs: PreTransform, - resultType: Type = rtLongClassType): TailRec[Tree] = { - pretransformApply(ApplyFlags.empty, lhs, MethodIdent(methodName), rhs :: Nil, - resultType, isStat = false, usePreTransform = true)( - cont) + def expand(methodName: MethodName, targs: PreTransform*): TailRec[Tree] = { + val impl = staticCall(LongImpl.RuntimeLongClass, MemberNamespace.PublicStatic, methodName) + pretransformSingleDispatch(ApplyFlags.empty, impl, None, targs.toList, + isStat = false, usePreTransform = true)(cont)( + throw new AssertionError(s"failed to inline RuntimeLong method $methodName at $pos")) } pretrans match { @@ -3616,19 +3533,33 @@ private[optimizer] abstract class OptimizerCore( (op: @switch) match { case IntToLong => - expandLongModuleOp(LongImpl.fromInt, arg) + expand(LongImpl.fromInt, arg) case LongToInt => - expandUnaryOp(LongImpl.toInt, arg, IntType) + expand(LongImpl.toInt, arg) case LongToDouble => - expandUnaryOp(LongImpl.toDouble, arg, DoubleType) + expand(LongImpl.toDouble, arg) case DoubleToLong => - expandLongModuleOp(LongImpl.fromDouble, arg) + expand(LongImpl.fromDouble, arg) case LongToFloat => - expandUnaryOp(LongImpl.toFloat, arg, FloatType) + expand(LongImpl.toFloat, arg) + + case Double_toBits if config.coreSpec.esFeatures.esVersion >= ESVersion.ES2015 => + expand(LongImpl.fromDoubleBits, + arg, PreTransTree(Transient(GetFPBitsDataView))) + + case Double_fromBits if config.coreSpec.esFeatures.esVersion >= ESVersion.ES2015 => + expand(LongImpl.bitsToDouble, + arg, PreTransTree(Transient(GetFPBitsDataView))) + + case Long_clz => + expand(LongImpl.clz, arg) + + case UnsignedIntToLong => + expand(LongImpl.fromUnsignedInt, arg) case _ => cont(pretrans) @@ -3638,34 +3569,34 @@ private[optimizer] abstract class OptimizerCore( import BinaryOp._ (op: @switch) match { - case Long_+ => expandBinaryOp(LongImpl.+, lhs, rhs) - - case Long_- => - lhs match { - case PreTransLit(LongLiteral(0L)) => - expandUnaryOp(LongImpl.UNARY_-, rhs) - case _ => - expandBinaryOp(LongImpl.-, lhs, rhs) - } - - case Long_* => expandBinaryOp(LongImpl.*, lhs, rhs) - case Long_/ => expandBinaryOp(LongImpl./, lhs, rhs) - case Long_% => expandBinaryOp(LongImpl.%, lhs, rhs) - - case Long_& => expandBinaryOp(LongImpl.&, lhs, rhs) - case Long_| => expandBinaryOp(LongImpl.|, lhs, rhs) - case Long_^ => expandBinaryOp(LongImpl.^, lhs, rhs) - - case Long_<< => expandBinaryOp(LongImpl.<<, lhs, rhs) - case Long_>>> => expandBinaryOp(LongImpl.>>>, lhs, rhs) - case Long_>> => expandBinaryOp(LongImpl.>>, lhs, rhs) - - case Long_== => expandBinaryOp(LongImpl.===, lhs, rhs) - case Long_!= => expandBinaryOp(LongImpl.!==, lhs, rhs) - case Long_< => expandBinaryOp(LongImpl.<, lhs, rhs) - case Long_<= => expandBinaryOp(LongImpl.<=, lhs, rhs) - case Long_> => expandBinaryOp(LongImpl.>, lhs, rhs) - case Long_>= => expandBinaryOp(LongImpl.>=, lhs, rhs) + case Long_+ => expand(LongImpl.add, lhs, rhs) + case Long_- => expand(LongImpl.sub, lhs, rhs) + case Long_* => expand(LongImpl.mul, lhs, rhs) + case Long_/ => expand(LongImpl.divide, lhs, rhs) + case Long_% => expand(LongImpl.remainder, lhs, rhs) + + case Long_& => expand(LongImpl.and, lhs, rhs) + case Long_| => expand(LongImpl.or, lhs, rhs) + case Long_^ => expand(LongImpl.xor, lhs, rhs) + + case Long_<< => expand(LongImpl.shl, lhs, rhs) + case Long_>>> => expand(LongImpl.shr, lhs, rhs) + case Long_>> => expand(LongImpl.sar, lhs, rhs) + + case Long_== => expand(LongImpl.equals_, lhs, rhs) + case Long_!= => expand(LongImpl.notEquals, lhs, rhs) + case Long_< => expand(LongImpl.lt, lhs, rhs) + case Long_<= => expand(LongImpl.le, lhs, rhs) + case Long_> => expand(LongImpl.gt, lhs, rhs) + case Long_>= => expand(LongImpl.ge, lhs, rhs) + + case Long_unsigned_/ => expand(LongImpl.divideUnsigned, lhs, rhs) + case Long_unsigned_% => expand(LongImpl.remainderUnsigned, lhs, rhs) + + case Long_unsigned_< => expand(LongImpl.ltu, lhs, rhs) + case Long_unsigned_<= => expand(LongImpl.leu, lhs, rhs) + case Long_unsigned_> => expand(LongImpl.gtu, lhs, rhs) + case Long_unsigned_>= => expand(LongImpl.geu, lhs, rhs) case _ => cont(pretrans) @@ -3702,6 +3633,11 @@ private[optimizer] abstract class OptimizerCore( case BinaryOp.Int_> => BinaryOp.Int_<= case BinaryOp.Int_>= => BinaryOp.Int_< + case BinaryOp.Int_unsigned_< => BinaryOp.Int_unsigned_>= + case BinaryOp.Int_unsigned_<= => BinaryOp.Int_unsigned_> + case BinaryOp.Int_unsigned_> => BinaryOp.Int_unsigned_<= + case BinaryOp.Int_unsigned_>= => BinaryOp.Int_unsigned_< + case BinaryOp.Long_== => BinaryOp.Long_!= case BinaryOp.Long_!= => BinaryOp.Long_== case BinaryOp.Long_< => BinaryOp.Long_>= @@ -3709,6 +3645,11 @@ private[optimizer] abstract class OptimizerCore( case BinaryOp.Long_> => BinaryOp.Long_<= case BinaryOp.Long_>= => BinaryOp.Long_< + case BinaryOp.Long_unsigned_< => BinaryOp.Long_unsigned_>= + case BinaryOp.Long_unsigned_<= => BinaryOp.Long_unsigned_> + case BinaryOp.Long_unsigned_> => BinaryOp.Long_unsigned_<= + case BinaryOp.Long_unsigned_>= => BinaryOp.Long_unsigned_< + case BinaryOp.Double_== => BinaryOp.Double_!= case BinaryOp.Double_!= => BinaryOp.Double_== @@ -3939,6 +3880,64 @@ private[optimizer] abstract class OptimizerCore( foldCast(default, ClassType(ClassClass, nullable = false)) } + // Floating point bit manipulation + + case Float_toBits => + arg match { + case PreTransLit(FloatLiteral(v)) => + PreTransLit(IntLiteral(java.lang.Float.floatToIntBits(v))) + case _ => + default + } + case Float_fromBits => + arg match { + case PreTransLit(IntLiteral(v)) => + PreTransLit(FloatLiteral(java.lang.Float.intBitsToFloat(v))) + case _ => + default + } + case Double_toBits => + arg match { + case PreTransLit(DoubleLiteral(v)) => + PreTransLit(LongLiteral(java.lang.Double.doubleToLongBits(v))) + case _ => + default + } + case Double_fromBits => + arg match { + case PreTransLit(LongLiteral(v)) => + PreTransLit(DoubleLiteral(java.lang.Double.longBitsToDouble(v))) + case _ => + default + } + + // clz + + case Int_clz => + arg match { + case PreTransLit(IntLiteral(v)) => + PreTransLit(IntLiteral(Integer.numberOfLeadingZeros(v))) + case _ => + default + } + case Long_clz => + arg match { + case PreTransLit(LongLiteral(v)) => + PreTransLit(IntLiteral(java.lang.Long.numberOfLeadingZeros(v))) + case _ => + default + } + + // Unsigned int to long + + case UnsignedIntToLong => + arg match { + case PreTransLit(IntLiteral(v)) => + PreTransLit(LongLiteral(Integer.toUnsignedLong(v))) + case _ => + default + } + case _ => default } @@ -4178,6 +4177,10 @@ private[optimizer] abstract class OptimizerCore( PreTransLit(IntLiteral(y)), z)) => foldBinaryOp(innerOp, PreTransLit(IntLiteral(x + y)), z) + // 1 + (-1 ^ x) == 1 + ~x == -x == 0 - x (this appears when optimizing a Range with step == -1) + case (PreTransLit(IntLiteral(1)), PreTransBinaryOp(Int_^, PreTransLit(IntLiteral(-1)), x)) => + foldBinaryOp(Int_-, PreTransLit(IntLiteral(0)), x) + case _ => default } @@ -4217,12 +4220,8 @@ private[optimizer] abstract class OptimizerCore( case 1 => rhs // Exact power of 2 - case _ if (x & (x - 1)) == 0 => - /* Note that this would match 0, but 0 is handled above. - * It will also match Int.MinValue, but that is not a problem - * as the optimization also works (if you need convincing, - * simply interpret the multiplication as unsigned). - */ + case _ if isUnsignedPowerOf2(x) => + // Interpret the multiplication as unsigned and turn it into a shift. foldBinaryOp(Int_<<, rhs, PreTransLit(IntLiteral(Integer.numberOfTrailingZeros(x)))) @@ -4247,6 +4246,33 @@ private[optimizer] abstract class OptimizerCore( case _ => default } + case Int_unsigned_/ => + (lhs, rhs) match { + case (_, PreTransLit(IntLiteral(0))) => + default + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(java.lang.Integer.divideUnsigned(l, r)) + + case (_, PreTransLit(IntLiteral(r))) if isUnsignedPowerOf2(r) => + foldBinaryOp(BinaryOp.Int_>>>, lhs, + PreTransLit(IntLiteral(java.lang.Integer.numberOfTrailingZeros(r)))) + + case _ => default + } + + case Int_unsigned_% => + (lhs, rhs) match { + case (_, PreTransLit(IntLiteral(0))) => + default + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(java.lang.Integer.remainderUnsigned(l, r)) + + case (_, PreTransLit(IntLiteral(r))) if isUnsignedPowerOf2(r) => + foldBinaryOp(BinaryOp.Int_&, PreTransLit(IntLiteral(r - 1)), lhs) + + case _ => default + } + case Int_% => (lhs, rhs) match { case (_, PreTransLit(IntLiteral(0))) => @@ -4275,6 +4301,20 @@ private[optimizer] abstract class OptimizerCore( PreTransBinaryOp(Int_|, PreTransLit(IntLiteral(y)), z)) => foldBinaryOp(Int_|, PreTransLit(IntLiteral(x | y)), z) + case (PreTransLit(IntLiteral(x)), _) => + val rhs2 = simplifyOnlyInterestedInMask(rhs, ~x) + if (rhs2 eq rhs) + default + else + foldBinaryOp(Int_|, lhs, rhs2) + + // x | (~x & z) --> x | z (appears in the inlining of 0L - b) + case (PreTransLocalDef(x), + PreTransBinaryOp(Int_&, + PreTransBinaryOp(Int_^, PreTransLit(IntLiteral(-1)), PreTransLocalDef(y)), + z)) if x eq y => + foldBinaryOp(Int_|, lhs, z) + case _ => default } @@ -4293,6 +4333,13 @@ private[optimizer] abstract class OptimizerCore( PreTransBinaryOp(Int_&, PreTransLit(IntLiteral(y)), z)) => foldBinaryOp(Int_&, PreTransLit(IntLiteral(x & y)), z) + case (PreTransLit(IntLiteral(x)), _) => + val rhs2 = simplifyOnlyInterestedInMask(rhs, x) + if (rhs2 eq rhs) + default + else + foldBinaryOp(Int_&, lhs, rhs2) + case _ => default } @@ -4329,10 +4376,15 @@ private[optimizer] abstract class OptimizerCore( case (_, PreTransLit(IntLiteral(y))) => val dist = y & 31 - if (dist == 0) + if (dist == 0) { lhs - else - PreTransBinaryOp(Int_<<, lhs, PreTransLit(IntLiteral(dist))) + } else { + val lhs2 = simplifyOnlyInterestedInMask(lhs, (-1) >>> dist) + if (lhs2 eq lhs) + PreTransBinaryOp(Int_<<, lhs, PreTransLit(IntLiteral(dist))) + else + foldBinaryOp(Int_<<, lhs2, PreTransLit(IntLiteral(dist))) + } case _ => default } @@ -4351,7 +4403,7 @@ private[optimizer] abstract class OptimizerCore( if (dist >= 32) PreTransTree(Block(finishTransformStat(x), IntLiteral(0))) else - PreTransBinaryOp(Int_>>>, x, PreTransLit(IntLiteral(dist))) + foldBinaryOp(Int_>>>, x, PreTransLit(IntLiteral(dist))) case (PreTransBinaryOp(op @ (Int_| | Int_& | Int_^), PreTransLit(IntLiteral(x)), y), @@ -4363,10 +4415,15 @@ private[optimizer] abstract class OptimizerCore( case (_, PreTransLit(IntLiteral(y))) => val dist = y & 31 - if (dist == 0) + if (dist == 0) { lhs - else - PreTransBinaryOp(Int_>>>, lhs, PreTransLit(IntLiteral(dist))) + } else { + val lhs2 = simplifyOnlyInterestedInMask(lhs, (-1) << dist) + if (lhs2 eq lhs) + PreTransBinaryOp(Int_>>>, lhs, PreTransLit(IntLiteral(dist))) + else + foldBinaryOp(Int_>>>, lhs2, PreTransLit(IntLiteral(dist))) + } case _ => default } @@ -4382,18 +4439,31 @@ private[optimizer] abstract class OptimizerCore( case (PreTransBinaryOp(Int_>>, x, PreTransLit(IntLiteral(y))), PreTransLit(IntLiteral(z))) => val dist = Math.min((y & 31) + (z & 31), 31) - PreTransBinaryOp(Int_>>, x, PreTransLit(IntLiteral(dist))) + foldBinaryOp(Int_>>, x, PreTransLit(IntLiteral(dist))) case (PreTransBinaryOp(Int_>>>, x, PreTransLit(IntLiteral(y))), PreTransLit(IntLiteral(_))) if (y & 31) != 0 => foldBinaryOp(Int_>>>, lhs, rhs) + case (PreTransBinaryOp(op @ (Int_| | Int_& | Int_^), + PreTransLit(IntLiteral(x)), y), + z @ PreTransLit(IntLiteral(zValue))) => + foldBinaryOp( + op, + PreTransLit(IntLiteral(x >> zValue)), + foldBinaryOp(Int_>>, y, z)) + case (_, PreTransLit(IntLiteral(y))) => val dist = y & 31 - if (dist == 0) + if (dist == 0) { lhs - else - PreTransBinaryOp(Int_>>, lhs, PreTransLit(IntLiteral(dist))) + } else { + val lhs2 = simplifyOnlyInterestedInMask(lhs, (-1) << dist) + if (lhs2 eq lhs) + PreTransBinaryOp(Int_>>, lhs, PreTransLit(IntLiteral(dist))) + else + foldBinaryOp(Int_>>, lhs2, PreTransLit(IntLiteral(dist))) + } case _ => default } @@ -4415,57 +4485,81 @@ private[optimizer] abstract class OptimizerCore( PreTransLit(IntLiteral(z))) => foldBinaryOp(op, y, PreTransLit(IntLiteral(x ^ z))) + case (PreTransLocalDef(l), PreTransLocalDef(r)) if l eq r => + booleanLit(op == Int_==) + case (PreTransLit(_), _) => foldBinaryOp(op, rhs, lhs) case _ => default } - case Int_< | Int_<= | Int_> | Int_>= => - def flippedOp = (op: @switch) match { - case Int_< => Int_> - case Int_<= => Int_>= - case Int_> => Int_< - case Int_>= => Int_<= + case Int_< | Int_<= | Int_> | Int_>= | + Int_unsigned_< | Int_unsigned_<= | Int_unsigned_> | Int_unsigned_>= => + val (isSigned, otherSignOp, flippedOp) = (op: @switch) match { + case Int_< => (true, Int_unsigned_<, Int_>) + case Int_<= => (true, Int_unsigned_<=, Int_>=) + case Int_> => (true, Int_unsigned_>, Int_<) + case Int_>= => (true, Int_unsigned_>=, Int_<=) + case Int_unsigned_< => (false, Int_<, Int_unsigned_>) + case Int_unsigned_<= => (false, Int_<=, Int_unsigned_>=) + case Int_unsigned_> => (false, Int_>, Int_unsigned_<) + case Int_unsigned_>= => (false, Int_>=, Int_unsigned_<=) } + val opMinValue = if (isSigned) Int.MinValue else 0 + val opMaxValue = if (isSigned) Int.MaxValue else -1 + val signedOp = if (isSigned) op else otherSignOp // for normalized tests + (lhs, rhs) match { case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => booleanLit((op: @switch) match { - case Int_< => l < r - case Int_<= => l <= r - case Int_> => l > r - case Int_>= => l >= r + case Int_< => l < r + case Int_<= => l <= r + case Int_> => l > r + case Int_>= => l >= r + case Int_unsigned_< => Integer.compareUnsigned(l, r) < 0 + case Int_unsigned_<= => Integer.compareUnsigned(l, r) <= 0 + case Int_unsigned_> => Integer.compareUnsigned(l, r) > 0 + case Int_unsigned_>= => Integer.compareUnsigned(l, r) >= 0 }) + case (IntFlipSign(x), PreTransLit(IntLiteral(r))) => + foldBinaryOp(otherSignOp, x, PreTransLit(IntLiteral(r ^ Int.MinValue)(rhs.pos))) + case (IntFlipSign(x), IntFlipSign(y)) => + foldBinaryOp(otherSignOp, x, y) + case (_, PreTransLit(IntLiteral(y))) => y match { - case Int.MinValue => - if (op == Int_< || op == Int_>=) { + case `opMinValue` => + if (signedOp == Int_< || signedOp == Int_>=) { Block(finishTransformStat(lhs), - BooleanLiteral(op == Int_>=)).toPreTransform + BooleanLiteral(signedOp == Int_>=)).toPreTransform } else { - foldBinaryOp(if (op == Int_<=) Int_== else Int_!=, lhs, rhs) + foldBinaryOp(if (signedOp == Int_<=) Int_== else Int_!=, lhs, rhs) } - case Int.MaxValue => - if (op == Int_> || op == Int_<=) { + case `opMaxValue` => + if (signedOp == Int_> || signedOp == Int_<=) { Block(finishTransformStat(lhs), - BooleanLiteral(op == Int_<=)).toPreTransform + BooleanLiteral(signedOp == Int_<=)).toPreTransform } else { - foldBinaryOp(if (op == Int_>=) Int_== else Int_!=, lhs, rhs) + foldBinaryOp(if (signedOp == Int_>=) Int_== else Int_!=, lhs, rhs) } - case _ if y == Int.MinValue + 1 && (op == Int_< || op == Int_>=) => - foldBinaryOp(if (op == Int_<) Int_== else Int_!=, lhs, - PreTransLit(IntLiteral(Int.MinValue))) + case _ if y == opMinValue + 1 && (signedOp == Int_< || signedOp == Int_>=) => + foldBinaryOp(if (signedOp == Int_<) Int_== else Int_!=, lhs, + PreTransLit(IntLiteral(opMinValue))) - case _ if y == Int.MaxValue - 1 && (op == Int_> || op == Int_<=) => - foldBinaryOp(if (op == Int_>) Int_== else Int_!=, lhs, - PreTransLit(IntLiteral(Int.MaxValue))) + case _ if y == opMaxValue - 1 && (signedOp == Int_> || signedOp == Int_<=) => + foldBinaryOp(if (signedOp == Int_>) Int_== else Int_!=, lhs, + PreTransLit(IntLiteral(opMaxValue))) case _ => default } + case (PreTransLocalDef(l), PreTransLocalDef(r)) if l eq r => + booleanLit(signedOp == Int_<= || signedOp == Int_>=) + case (PreTransLit(IntLiteral(_)), _) => foldBinaryOp(flippedOp, rhs, lhs) @@ -4525,12 +4619,8 @@ private[optimizer] abstract class OptimizerCore( case 1L => rhs // Exact power of 2 - case _ if (x & (x - 1L)) == 0L => - /* Note that this would match 0L, but 0L is handled above. - * It will also match Long.MinValue, but that is not a problem - * as the optimization also works (if you need convincing, - * simply interpret the multiplication as unsigned). - */ + case _ if isUnsignedPowerOf2(x) => + // Interpret the multiplication as unsigned and turn it into a shift. foldBinaryOp(Long_<<, rhs, PreTransLit( IntLiteral(java.lang.Long.numberOfTrailingZeros(x)))) @@ -4547,10 +4637,10 @@ private[optimizer] abstract class OptimizerCore( case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => longLit(l / r) - case (_, PreTransLit(LongLiteral(1))) => + case (_, PreTransLit(LongLiteral(1L))) => lhs - case (_, PreTransLit(LongLiteral(-1))) => - foldBinaryOp(Long_-, PreTransLit(LongLiteral(0)), lhs) + case (_, PreTransLit(LongLiteral(-1L))) => + foldBinaryOp(Long_-, PreTransLit(LongLiteral(0L)), lhs) case (LongFromInt(x), LongFromInt(PreTransLit(y: IntLiteral))) if y.value != -1 => @@ -4575,6 +4665,33 @@ private[optimizer] abstract class OptimizerCore( case _ => default } + case Long_unsigned_/ => + (lhs, rhs) match { + case (_, PreTransLit(LongLiteral(0L))) => + default + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(java.lang.Long.divideUnsigned(l, r)) + + case (_, PreTransLit(LongLiteral(r))) if isUnsignedPowerOf2(r) => + foldBinaryOp(BinaryOp.Long_>>>, lhs, + PreTransLit(IntLiteral(java.lang.Long.numberOfTrailingZeros(r)))) + + case _ => default + } + + case Long_unsigned_% => + (lhs, rhs) match { + case (_, PreTransLit(LongLiteral(0L))) => + default + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(java.lang.Long.remainderUnsigned(l, r)) + + case (_, PreTransLit(LongLiteral(r))) if isUnsignedPowerOf2(r) => + foldBinaryOp(BinaryOp.Long_&, PreTransLit(LongLiteral(r - 1L)), lhs) + + case _ => default + } + case Long_| => (lhs, rhs) match { case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => @@ -4608,6 +4725,9 @@ private[optimizer] abstract class OptimizerCore( case (PreTransLit(LongLiteral(0)), _) => PreTransBlock(finishTransformStat(rhs), lhs) + case (PreTransLit(LongLiteral(0xffffffffL)), LongFromInt(intRhs)) => + foldUnaryOp(UnaryOp.UnsignedIntToLong, intRhs) + case (PreTransLit(LongLiteral(x)), PreTransBinaryOp(Long_&, PreTransLit(LongLiteral(y)), z)) => foldBinaryOp(Long_&, PreTransLit(LongLiteral(x & y)), z) @@ -4686,54 +4806,68 @@ private[optimizer] abstract class OptimizerCore( PreTransLit(LongLiteral(z))) => foldBinaryOp(op, y, PreTransLit(LongLiteral(x ^ z))) + case (PreTransLocalDef(l), PreTransLocalDef(r)) if l eq r => + booleanLit(positive) + case (PreTransLit(LongLiteral(_)), _) => foldBinaryOp(op, rhs, lhs) case _ => default } - case Long_< | Long_<= | Long_> | Long_>= => - def flippedOp = (op: @switch) match { - case Long_< => Long_> - case Long_<= => Long_>= - case Long_> => Long_< - case Long_>= => Long_<= + case Long_< | Long_<= | Long_> | Long_>= | + Long_unsigned_< | Long_unsigned_<= | Long_unsigned_> | Long_unsigned_>= => + val (isSigned, otherSignOp, flippedOp, intOp) = (op: @switch) match { + case Long_< => (true, Long_unsigned_<, Long_>, Int_<) + case Long_<= => (true, Long_unsigned_<=, Long_>=, Int_<=) + case Long_> => (true, Long_unsigned_>, Long_<, Int_>) + case Long_>= => (true, Long_unsigned_>=, Long_<=, Int_>=) + case Long_unsigned_< => (false, Long_<, Long_unsigned_>, Int_unsigned_<) + case Long_unsigned_<= => (false, Long_<=, Long_unsigned_>=, Int_unsigned_<=) + case Long_unsigned_> => (false, Long_>, Long_unsigned_<, Int_unsigned_>) + case Long_unsigned_>= => (false, Long_>=, Long_unsigned_<=, Int_unsigned_>=) } - def intOp = (op: @switch) match { - case Long_< => Int_< - case Long_<= => Int_<= - case Long_> => Int_> - case Long_>= => Int_>= - } + val opMinValue = if (isSigned) Long.MinValue else 0L + val opMaxValue = if (isSigned) Long.MaxValue else -1L + val signedOp = if (isSigned) op else otherSignOp // for normalized tests (lhs, rhs) match { case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => booleanLit((op: @switch) match { - case Long_< => l < r - case Long_<= => l <= r - case Long_> => l > r - case Long_>= => l >= r + case Long_< => l < r + case Long_<= => l <= r + case Long_> => l > r + case Long_>= => l >= r + case Long_unsigned_< => java.lang.Long.compareUnsigned(l, r) < 0 + case Long_unsigned_<= => java.lang.Long.compareUnsigned(l, r) <= 0 + case Long_unsigned_> => java.lang.Long.compareUnsigned(l, r) > 0 + case Long_unsigned_>= => java.lang.Long.compareUnsigned(l, r) >= 0 }) - case (_, PreTransLit(LongLiteral(Long.MinValue))) => - if (op == Long_< || op == Long_>=) { + case (LongFlipSign(x), PreTransLit(LongLiteral(r))) => + foldBinaryOp(otherSignOp, x, PreTransLit(LongLiteral(r ^ Long.MinValue)(rhs.pos))) + case (LongFlipSign(x), LongFlipSign(y)) => + foldBinaryOp(otherSignOp, x, y) + + case (_, PreTransLit(LongLiteral(`opMinValue`))) => + if (signedOp == Long_< || signedOp == Long_>=) { Block(finishTransformStat(lhs), - BooleanLiteral(op == Long_>=)).toPreTransform + BooleanLiteral(signedOp == Long_>=)).toPreTransform } else { - foldBinaryOp(if (op == Long_<=) Long_== else Long_!=, lhs, rhs) + foldBinaryOp(if (signedOp == Long_<=) Long_== else Long_!=, lhs, rhs) } - case (_, PreTransLit(LongLiteral(Long.MaxValue))) => - if (op == Long_> || op == Long_<=) { + case (_, PreTransLit(LongLiteral(`opMaxValue`))) => + if (signedOp == Long_> || signedOp == Long_<=) { Block(finishTransformStat(lhs), - BooleanLiteral(op == Long_<=)).toPreTransform + BooleanLiteral(signedOp == Long_<=)).toPreTransform } else { - foldBinaryOp(if (op == Long_>=) Long_== else Long_!=, lhs, rhs) + foldBinaryOp(if (signedOp == Long_>=) Long_== else Long_!=, lhs, rhs) } case (LongFromInt(x), LongFromInt(y)) => foldBinaryOp(intOp, x, y) - case (LongFromInt(x), PreTransLit(LongLiteral(y))) => + case (LongFromInt(x), PreTransLit(LongLiteral(y))) if isSigned => assert(y > Int.MaxValue || y < Int.MinValue) val result = if (y > Int.MaxValue) op == Long_< || op == Long_<= @@ -4747,7 +4881,8 @@ private[optimizer] abstract class OptimizerCore( */ case (PreTransBinaryOp(Long_+, PreTransLit(LongLiteral(x)), y @ LongFromInt(_)), PreTransLit(LongLiteral(z))) - if canAddLongs(x, Int.MinValue) && + if isSigned && + canAddLongs(x, Int.MinValue) && canAddLongs(x, Int.MaxValue) && canSubtractLongs(z, x) => foldBinaryOp(op, y, PreTransLit(LongLiteral(z-x))) @@ -4759,7 +4894,8 @@ private[optimizer] abstract class OptimizerCore( */ case (PreTransBinaryOp(Long_-, PreTransLit(LongLiteral(x)), y @ LongFromInt(_)), PreTransLit(LongLiteral(z))) - if canSubtractLongs(x, Int.MinValue) && + if isSigned && + canSubtractLongs(x, Int.MinValue) && canSubtractLongs(x, Int.MaxValue) && canSubtractLongs(z, x) => if (z-x != Long.MinValue) { @@ -4787,7 +4923,8 @@ private[optimizer] abstract class OptimizerCore( * This requires to evaluate x and y once. */ case (PreTransBinaryOp(Long_+, LongFromInt(x), LongFromInt(y)), - PreTransLit(LongLiteral(Int.MaxValue))) => + PreTransLit(LongLiteral(Int.MaxValue))) + if isSigned => trampoline { /* HACK: We use an empty scope here for `withNewLocalDefs`. * It's OKish to do that because we're only defining Ints, and @@ -4809,6 +4946,9 @@ private[optimizer] abstract class OptimizerCore( } (finishTransform(isStat = false))(emptyScope) }.toPreTransform + case (PreTransLocalDef(l), PreTransLocalDef(r)) if l eq r => + booleanLit(signedOp == Long_<= || signedOp == Long_>=) + case (PreTransLit(LongLiteral(_)), _) => foldBinaryOp(flippedOp, rhs, lhs) @@ -5008,6 +5148,84 @@ private[optimizer] abstract class OptimizerCore( } } + /** Simplifies the given `value` expression with the knowledge that only some + * of its resulting bits will be relevant. + * + * The relevant bits are those that are 1 in `mask`. These bits must be + * preserved by the simplifications. Bits that are 0 in `mask` can be + * arbitrarily altered. + * + * For an example of why this is useful, consider Long addition where `a` + * is a constant. The formula for the `hi` result contains the following + * subexpression: + * {{{ + * ((alo & blo) | ((alo | blo) & ~lo)) >>> 31 + * }}} + * + * Since we are going to shift by >>> 31, only the most significant bit + * (msb) of the left-hand-side is relevant. We can alter the other ones. + * Since `a` is constant, `alo` is constant. If it were equal to 0, the + * leftmost `&` and the innermost `|` would fold away. It is unfortunately + * often not 0. The end result only depends on its msb, however, and that's + * where this simplification helps. + * + * If the msb of `alo` is 0, we can replace `alo` in that subexpression by 0 + * without altering the final result. That allows parts of the expression to + * fold away. + * + * Likewise, if its msb is 1, we can replace `alo` by -1. That also allows + * to fold the leftmost `&` and the innermost `|` (in different ways). + * + * The simplification performed in this method is capable of performing that + * rewrite. It pushes the relevant masking information down combinations of + * `&`, `|` and `^`, and rewrites constants in the way that allows the most + * folding without altering the end result. + * + * When we cannot improve a fold, we transform constants so that they are + * closer to 0. This is a code size improvement. Constants close to 0 use + * fewer bytes in the final encoding (textual in JS, signed LEB in Wasm). + */ + private def simplifyOnlyInterestedInMask(value: PreTransform, mask: Int): PreTransform = { + import BinaryOp._ + + implicit val pos = value.pos + + def chooseSmallestAbs(a: Int, b: Int): Int = + if (Integer.compareUnsigned(Math.abs(a), Math.abs(b)) <= 0) a + else b + + value match { + case PreTransBinaryOp(op @ (Int_& | Int_| | Int_^), lhs, rhs) => + def simplifyArg(arg: PreTransform): PreTransform = { + simplifyOnlyInterestedInMask(arg, mask) match { + case arg2 @ PreTransLit(IntLiteral(v)) => + val improvedV = (v & mask) match { + case 0 => 0 // foldBinaryOp below will fold this away + case `mask` => -1 // same, except for Int_^, in which case it becomes the ~z representation + case masked => chooseSmallestAbs(masked, masked | ~mask) + } + if (improvedV == v) + arg2 + else + PreTransLit(IntLiteral(improvedV)(arg2.pos)) + case arg2 => + arg2 + } + } + + val lhs2 = simplifyArg(lhs) + val rhs2 = simplifyArg(rhs) + + if ((lhs2 eq lhs) && (rhs2 eq rhs)) + value + else + foldBinaryOp(op, lhs2, rhs2) + + case _ => + value + } + } + private def fold3WayIntComparison(canBeEqual: Boolean, canBeLessThan: Boolean, canBeGreaterThan: Boolean, lhs: PreTransform, rhs: PreTransform)( implicit pos: Position): PreTransform = { @@ -5666,6 +5884,12 @@ private[optimizer] object OptimizerCore { private val ClassTagApplyMethodName = MethodName("apply", List(ClassRef(ClassClass)), ClassRef(ClassName("scala.reflect.ClassTag"))) + def isUnsignedPowerOf2(x: Int): Boolean = + (x & (x - 1)) == 0 && x != 0 + + def isUnsignedPowerOf2(x: Long): Boolean = + (x & (x - 1L)) == 0L && x != 0L + final class InlineableClassStructure(val className: ClassName, private val allFields: List[FieldDef]) { private[OptimizerCore] val refinedType: RefinedType = RefinedType(ClassType(className, nullable = false), isExact = true) @@ -6379,6 +6603,24 @@ private[optimizer] object OptimizerCore { } } + private object IntFlipSign { + def unapply(tree: PreTransform): Option[PreTransform] = tree match { + case PreTransBinaryOp(BinaryOp.Int_^, PreTransLit(IntLiteral(Int.MinValue)), x) => + Some(x) + case _ => + None + } + } + + private object LongFlipSign { + def unapply(tree: PreTransform): Option[PreTransform] = tree match { + case PreTransBinaryOp(BinaryOp.Long_^, PreTransLit(LongLiteral(Long.MinValue)), x) => + Some(x) + case _ => + None + } + } + private object AndThen { def apply(lhs: Tree, rhs: Tree)(implicit pos: Position): Tree = If(lhs, rhs, BooleanLiteral(false))(BooleanType) @@ -6473,31 +6715,19 @@ private[optimizer] object OptimizerCore { final val ArrayUpdate = ArrayApply + 1 final val ArrayLength = ArrayUpdate + 1 - final val IntegerNLZ = ArrayLength + 1 - final val IntegerNTZ = IntegerNLZ + 1 + final val IntegerNTZ = ArrayLength + 1 final val IntegerBitCount = IntegerNTZ + 1 final val IntegerRotateLeft = IntegerBitCount + 1 final val IntegerRotateRight = IntegerRotateLeft + 1 - final val IntegerDivideUnsigned = IntegerRotateRight + 1 - final val IntegerRemainderUnsigned = IntegerDivideUnsigned + 1 - final val LongNLZ = IntegerRemainderUnsigned + 1 - final val LongNTZ = LongNLZ + 1 + final val LongNTZ = IntegerRotateRight + 1 final val LongBitCount = LongNTZ + 1 final val LongRotateLeft = LongBitCount + 1 final val LongRotateRight = LongRotateLeft + 1 final val LongToString = LongRotateRight + 1 final val LongCompare = LongToString + 1 - final val LongDivideUnsigned = LongCompare + 1 - final val LongRemainderUnsigned = LongDivideUnsigned + 1 - - final val FloatToIntBits = LongRemainderUnsigned + 1 - final val IntBitsToFloat = FloatToIntBits + 1 - final val DoubleToLongBits = IntBitsToFloat + 1 - final val LongBitsToDouble = DoubleToLongBits + 1 - - final val CharacterCodePointToString = LongBitsToDouble + 1 + final val CharacterCodePointToString = LongCompare + 1 final val StringCodePointAt = CharacterCodePointToString + 1 final val StringSubstringStart = StringCodePointAt + 1 @@ -6513,8 +6743,9 @@ private[optimizer] object OptimizerCore { final val MathMinDouble = MathMinFloat + 1 final val MathMaxFloat = MathMinDouble + 1 final val MathMaxDouble = MathMaxFloat + 1 + final val MathMultiplyFull = MathMaxDouble + 1 - final val ArrayBuilderZeroOf = MathMaxDouble + 1 + final val ArrayBuilderZeroOf = MathMultiplyFull + 1 final val GenericArrayBuilderResult = ArrayBuilderZeroOf + 1 final val ClassGetName = GenericArrayBuilderResult + 1 @@ -6568,9 +6799,6 @@ private[optimizer] object OptimizerCore { m("array_update", List(O, I, O), V) -> ArrayUpdate, m("array_length", List(O), I) -> ArrayLength ), - ClassName("java.lang.Integer$") -> List( - m("numberOfLeadingZeros", List(I), I) -> IntegerNLZ - ), ClassName("java.lang.Class") -> List( m("getName", Nil, StringClassRef) -> ClassGetName ), @@ -6605,38 +6833,25 @@ private[optimizer] object OptimizerCore { private val runtimeLongIntrinsics: List[(ClassName, List[(MethodName, Int)])] = List( ClassName("java.lang.Long$") -> List( m("toString", List(J), ClassRef(BoxedStringClass)) -> LongToString, - m("compare", List(J, J), I) -> LongCompare, - m("divideUnsigned", List(J, J), J) -> LongDivideUnsigned, - m("remainderUnsigned", List(J, J), J) -> LongRemainderUnsigned + m("compare", List(J, J), I) -> LongCompare + ), + ClassName("java.lang.Math$") -> List( + m("multiplyFull", List(I, I), J) -> MathMultiplyFull ) ) private val wasmIntrinsics: List[(ClassName, List[(MethodName, Int)])] = List( ClassName("java.lang.Integer$") -> List( - // note: numberOfLeadingZeros in already in the commonIntrinsics m("numberOfTrailingZeros", List(I), I) -> IntegerNTZ, m("bitCount", List(I), I) -> IntegerBitCount, m("rotateLeft", List(I, I), I) -> IntegerRotateLeft, - m("rotateRight", List(I, I), I) -> IntegerRotateRight, - m("divideUnsigned", List(I, I), I) -> IntegerDivideUnsigned, - m("remainderUnsigned", List(I, I), I) -> IntegerRemainderUnsigned + m("rotateRight", List(I, I), I) -> IntegerRotateRight ), ClassName("java.lang.Long$") -> List( - m("numberOfLeadingZeros", List(J), I) -> LongNLZ, m("numberOfTrailingZeros", List(J), I) -> LongNTZ, m("bitCount", List(J), I) -> LongBitCount, m("rotateLeft", List(J, I), J) -> LongRotateLeft, - m("rotateRight", List(J, I), J) -> LongRotateRight, - m("divideUnsigned", List(J, J), J) -> LongDivideUnsigned, - m("remainderUnsigned", List(J, J), J) -> LongRemainderUnsigned - ), - ClassName("java.lang.Float$") -> List( - m("floatToIntBits", List(F), I) -> FloatToIntBits, - m("intBitsToFloat", List(I), F) -> IntBitsToFloat - ), - ClassName("java.lang.Double$") -> List( - m("doubleToLongBits", List(D), J) -> DoubleToLongBits, - m("longBitsToDouble", List(J), D) -> LongBitsToDouble + m("rotateRight", List(J, I), J) -> LongRotateRight ), ClassName("java.lang.Character$") -> List( m("toString", List(I), StringClassRef) -> CharacterCodePointToString diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/CoreSpec.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/CoreSpec.scala index e5e285268f..3c4c979adc 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/CoreSpec.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/CoreSpec.scala @@ -96,9 +96,6 @@ final class CoreSpec private ( targetIsWebAssembly ) } - - private[linker] lazy val linkTimeProperties = new LinkTimeProperties( - semantics, esFeatures, targetIsWebAssembly) } private[linker] object CoreSpec { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/SymbolRequirement.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/SymbolRequirement.scala index 6838c8341c..5483265491 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/SymbolRequirement.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/SymbolRequirement.scala @@ -79,6 +79,11 @@ object SymbolRequirement { CallStaticMethod(origin, className, methodName) } + def callStaticMethods(className: ClassName, + methodNames: List[MethodName]): SymbolRequirement = { + multipleInternal(methodNames.map(callStaticMethod(className, _))) + } + @deprecated("broken (not actually optional), do not use", "1.13.2") def optional(requirement: SymbolRequirement): SymbolRequirement = requirement diff --git a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala index 4eb535144d..c543be0f2b 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala @@ -874,6 +874,114 @@ class AnalyzerTest { ) Future.sequence(results) } + + @Test + def linkTimeIfReachable(): AsyncResult = await { + val mainMethodName = m("main", Nil, IntRef) + val fooMethodName = m("foo", Nil, IntRef) + val barMethodName = m("bar", Nil, IntRef) + + val thisType = ClassType("A", nullable = false) + + val productionMode = true + + /* linkTimeIf(productionMode) { + * this.foo() + * } { + * this.bar() + * } + */ + val mainBody = LinkTimeIf( + BinaryOp(BinaryOp.Boolean_==, + LinkTimeProperty("core/productionMode")(BooleanType), + BooleanLiteral(productionMode)), + Apply(EAF, This()(thisType), fooMethodName, Nil)(IntType), + Apply(EAF, This()(thisType), barMethodName, Nil)(IntType) + )(IntType) + + val classDefs = Seq( + classDef("A", superClass = Some(ObjectClass), + methods = List( + trivialCtor("A"), + MethodDef(EMF, mainMethodName, NON, Nil, IntType, Some(mainBody))(EOH, UNV), + MethodDef(EMF, fooMethodName, NON, Nil, IntType, Some(int(1)))(EOH, UNV), + MethodDef(EMF, barMethodName, NON, Nil, IntType, Some(int(2)))(EOH, UNV) + ) + ) + ) + + val requirements = { + reqsFactory.instantiateClass("A", NoArgConstructorName) ++ + reqsFactory.callMethod("A", mainMethodName) + } + + val analysisFuture = computeAnalysis(classDefs, requirements, + config = StandardConfig().withSemantics(_.withProductionMode(productionMode))) + + for (analysis <- analysisFuture) yield { + assertNoError(analysis) + + val AfooMethodInfo = analysis.classInfos("A") + .methodInfos(MemberNamespace.Public)(fooMethodName) + assertTrue(AfooMethodInfo.isReachable) + + val AbarMethodInfo = analysis.classInfos("A") + .methodInfos(MemberNamespace.Public)(barMethodName) + assertFalse(AbarMethodInfo.isReachable) + } + } + + @Test + def linkTimeIfError(): AsyncResult = await { + val mainMethodName = m("main", Nil, IntRef) + val fooMethodName = m("foo", Nil, IntRef) + + val thisType = ClassType("A", nullable = false) + + val productionMode = true + + /* linkTimeIf(unknownProperty) { + * this.foo() + * } { + * this.bar() + * } + */ + val mainBody = LinkTimeIf( + BinaryOp(BinaryOp.Boolean_==, + LinkTimeProperty("core/unknownProperty")(BooleanType), + BooleanLiteral(productionMode)), + Apply(EAF, This()(thisType), fooMethodName, Nil)(IntType), + Apply(EAF, This()(thisType), fooMethodName, Nil)(IntType) + )(IntType) + + val classDefs = Seq( + classDef("A", superClass = Some(ObjectClass), + methods = List( + trivialCtor("A"), + MethodDef(EMF, mainMethodName, NON, Nil, IntType, Some(mainBody))(EOH, UNV) + ) + ) + ) + + val requirements = { + reqsFactory.instantiateClass("A", NoArgConstructorName) ++ + reqsFactory.callMethod("A", mainMethodName) + } + + val analysisFuture = computeAnalysis(classDefs, requirements, + config = StandardConfig().withSemantics(_.withProductionMode(productionMode))) + + for (analysis <- analysisFuture) yield { + assertContainsError(s"InvalidLinkTimeProperty(core/unknownProperty)", analysis) { + case InvalidLinkTimeProperty("core/unknownProperty", BooleanType, _) => true + } + + // Branches are not taken, so there is no error for linking `foo` + assertNotContainsError(s"any MissingMethod", analysis) { + case MissingMethod(_, _) => true + } + } + } } object AnalyzerTest { @@ -962,10 +1070,21 @@ object AnalyzerTest { private def assertContainsError(msg: String, analysis: Analysis)( pf: PartialFunction[Error, Boolean]): Unit = { - val fullMessage = s"Expected $msg, got ${analysis.errors}" - assertTrue(fullMessage, analysis.errors.exists { + assertTrue(s"Expected $msg, got ${analysis.errors}", + containsError(analysis)(pf)) + } + + private def assertNotContainsError(msg: String, analysis: Analysis)( + pf: PartialFunction[Error, Boolean]): Unit = { + assertFalse(s"Did not expect $msg, got ${analysis.errors}", + containsError(analysis)(pf)) + } + + private def containsError(analysis: Analysis)( + pf: PartialFunction[Error, Boolean]): Boolean = { + analysis.errors.exists { e => pf.applyOrElse(e, (_: Error) => false) - }) + } } object ClsInfo { diff --git a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala index 73dce25631..1c6ae731b1 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala @@ -446,6 +446,7 @@ object IRCheckerTest { new ClassTransformer { override def transform(tree: Tree): Tree = tree match { case tree: LinkTimeProperty => zeroOf(tree.tpe) + case tree: LinkTimeIf => zeroOf(tree.tpe) case tree: NewLambda => UnaryOp(UnaryOp.Throw, Null()) case _ => super.transform(tree) } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 137d8e7400..d2a7b193e6 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 147046, - expectedFullLinkSizeWithoutClosure = 85355, - expectedFullLinkSizeWithClosure = 21492, + expectedFastLinkSize = 148960, + expectedFullLinkSizeWithoutClosure = 88111, + expectedFullLinkSizeWithClosure = 20704, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala index 6441fd0c48..309cc5d7a1 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala @@ -834,6 +834,84 @@ class ClassDefCheckerTest { "Assignment to RecordSelect of illegal tree: org.scalajs.ir.Trees$IntLiteral", previousPhase = CheckingPhase.Optimizer) } + + @Test + def linkTimePropertyTest(): Unit = { + // Test that some illegal types are rejected + for (tpe <- List(FloatType, NullType, NothingType, ClassType(BoxedStringClass, nullable = false))) { + assertError( + mainTestClassDef(LinkTimeProperty("foo")(tpe)), + s"${tpe.show()} is not a valid type for LinkTimeProperty") + } + + // Some error also gets reported if used in link-time-tree position + assertError( + mainTestClassDef { + LinkTimeIf(LinkTimeProperty("foo")(NothingType), int(5), int(6))(IntType) + }, + s"boolean expected but nothing found in link-time tree") + + // LinkTimeProperty is rejected after desugaring + assertError( + mainTestClassDef(LinkTimeProperty("foo")(IntType)), + "Illegal link-time property 'foo' after desugaring", + previousPhase = CheckingPhase.Optimizer) + } + + @Test + def linkTimeIfTest(): Unit = { + def makeTestClassDef(cond: Tree): ClassDef = { + classDef( + "Foo", + superClass = Some(ObjectClass), + methods = List( + trivialCtor("Foo"), + MethodDef(EMF, MethodName("foo", Nil, VoidRef), NON, Nil, VoidType, Some { + LinkTimeIf( + cond, + consoleLog(StringLiteral("foo")), + consoleLog(StringLiteral("bar")) + )(VoidType) + })(EOH, UNV) + ) + ) + } + + assertError( + makeTestClassDef( + UnaryOp(UnaryOp.Boolean_!, int(0)) + ), + "boolean expected but int found in link-time tree" + ) + + assertError( + makeTestClassDef( + BinaryOp(BinaryOp.Int_==, int(0), LinkTimeProperty("core/productionMode")(BooleanType)) + ), + "int expected but boolean found in link-time tree" + ) + + assertError( + makeTestClassDef( + BinaryOp(BinaryOp.Boolean_==, int(0), LinkTimeProperty("core/productionMode")(BooleanType)) + ), + "boolean expected but int found in link-time tree" + ) + + assertError( + makeTestClassDef( + BinaryOp(BinaryOp.===, int(0), int(1)) + ), + "illegal binary op 1 in link-time tree" + ) + + assertError( + makeTestClassDef( + If(BooleanLiteral(true), BooleanLiteral(true), BooleanLiteral(false))(BooleanType) + ), + "illegal tree of class org.scalajs.ir.Trees$If in link-time tree" + ) + } } private object ClassDefCheckerTest { diff --git a/linker/shared/src/test/scala/org/scalajs/linker/frontend/modulesplitter/LinkTimeEvaluatorTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/frontend/modulesplitter/LinkTimeEvaluatorTest.scala new file mode 100644 index 0000000000..79e8d36ff6 --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/frontend/modulesplitter/LinkTimeEvaluatorTest.scala @@ -0,0 +1,102 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker.frontend + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ + +import org.scalajs.linker.interface.{ESFeatures, ESVersion, Semantics, StandardConfig} +import org.scalajs.linker.standard.CoreSpec +import org.scalajs.linker.testutils.TestIRBuilder._ + +class LinkTimeEvaluatorTest { + /** Convenience builder for `LinkTimeProperties` with mostly-default configs. */ + private def make( + semantics: Semantics => Semantics = identity, + esFeatures: ESFeatures => ESFeatures = identity, + isWebAssembly: Boolean = false + ): LinkTimeProperties = { + val config = StandardConfig() + .withSemantics(semantics) + .withESFeatures(esFeatures) + .withExperimentalUseWebAssembly(isWebAssembly) + LinkTimeProperties.fromCoreSpec(CoreSpec.fromStandardConfig(config)) + } + + @Test + def testTryEvalLinkTimeBooleanExpr(): Unit = { + val defaults = make() + + def test(expected: Option[Boolean], tree: Tree, config: LinkTimeProperties = defaults): Unit = + assertEquals(expected, LinkTimeEvaluator.tryEvalLinkTimeBooleanExpr(config, tree)) + + def testTrue(tree: Tree, config: LinkTimeProperties = defaults): Unit = + test(Some(true), tree, config) + + def testFalse(tree: Tree, config: LinkTimeProperties = defaults): Unit = + test(Some(false), tree, config) + + def testFail(tree: Tree, config: LinkTimeProperties = defaults): Unit = + test(None, tree, config) + + // Boolean literal + testTrue(bool(true)) + testFalse(bool(false)) + + // Boolean link-time property + testFalse(LinkTimeProperty("core/isWebAssembly")(BooleanType)) + testTrue(LinkTimeProperty("core/isWebAssembly")(BooleanType), make(isWebAssembly = true)) + testFail(LinkTimeProperty("core/missing")(BooleanType)) + testFail(LinkTimeProperty("core/esVersion")(BooleanType)) + + // Int comparison + for (l <- List(3, 5, 7); r <- List(3, 5, 7)) { + test(Some(l == r), BinaryOp(BinaryOp.Int_==, int(l), int(r))) + test(Some(l != r), BinaryOp(BinaryOp.Int_!=, int(l), int(r))) + test(Some(l < r), BinaryOp(BinaryOp.Int_<, int(l), int(r))) + test(Some(l <= r), BinaryOp(BinaryOp.Int_<=, int(l), int(r))) + test(Some(l > r), BinaryOp(BinaryOp.Int_>, int(l), int(r))) + test(Some(l >= r), BinaryOp(BinaryOp.Int_>=, int(l), int(r))) + } + + // Boolean operator + testTrue(UnaryOp(UnaryOp.Boolean_!, bool(false))) + testFalse(UnaryOp(UnaryOp.Boolean_!, bool(true))) + + // Comparison with link-time property + val esVersionProp = LinkTimeProperty("core/esVersion")(IntType) + testTrue(BinaryOp(BinaryOp.Int_>=, esVersionProp, int(ESVersion.ES2015.edition))) + testFalse(BinaryOp(BinaryOp.Int_>=, esVersionProp, int(ESVersion.ES2019.edition))) + testTrue(BinaryOp(BinaryOp.Int_>=, esVersionProp, int(ESVersion.ES2019.edition)), + make(esFeatures = _.withESVersion(ESVersion.ES2021))) + + // LinkTimeIf + testTrue(LinkTimeIf(bool(true), bool(true), bool(false))(BooleanType)) + testFalse(LinkTimeIf(bool(true), bool(false), bool(true))(BooleanType)) + testFalse(LinkTimeIf(bool(false), bool(true), bool(false))(BooleanType)) + + // Complex expression: esVersion >= ES2016 && esVersion <= ES2019 + val complexExpr = LinkTimeIf( + BinaryOp(BinaryOp.Int_>=, esVersionProp, int(ESVersion.ES2016.edition)), + BinaryOp(BinaryOp.Int_<=, esVersionProp, int(ESVersion.ES2019.edition)), + bool(false))( + BooleanType) + testTrue(complexExpr, make(esFeatures = _.withESVersion(ESVersion.ES2017))) + testTrue(complexExpr, make(esFeatures = _.withESVersion(ESVersion.ES2019))) + testFalse(complexExpr, make(esFeatures = _.withESVersion(ESVersion.ES2015))) + testFalse(complexExpr, make(esFeatures = _.withESVersion(ESVersion.ES2021))) + } +} diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala index 7d022a5123..a4284ec897 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala @@ -196,6 +196,7 @@ object TestIRBuilder { implicit def methodName2MethodIdent(name: MethodName): MethodIdent = MethodIdent(name) + def bool(x: Boolean): BooleanLiteral = BooleanLiteral(x) def int(x: Int): IntLiteral = IntLiteral(x) def str(x: String): StringLiteral = StringLiteral(x) } diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 5435860a02..2e94162e72 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -5,56 +5,15 @@ import com.typesafe.tools.mima.core.ProblemFilters._ object BinaryIncompatibilities { val IR = Seq( - // !!! Breaking, OK in minor release - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names.*Class"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names.ClassInitializerName"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names.DefaultModuleID"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names.HijackedClasses"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names.NoArgConstructorName"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names.ObjectArgConstructorName"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names.StaticInitializerName"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types.BoxedClassToPrimType"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types.PrimTypeToBoxedClass"), - - // !!! Breaking, OK in minor release - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.InvalidIRException.tree"), - ProblemFilters.exclude[Problem]("org.scalajs.ir.Trees#Closure.*"), - - // !!! Breaking, PrimRef is not a case class anymore - ProblemFilters.exclude[MissingTypesProblem]("org.scalajs.ir.Types$PrimRef"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.canEqual"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.productArity"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.productElement"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.productElementName"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.productElementNames"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.productIterator"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.productPrefix"), - ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.scalajs.ir.Types#PrimRef.unapply"), - - // !!! Breaking I guess ... we used to leak public things out of a `case class` with a private[ir] constructor - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.this"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.apply"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.copy"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimRef.copy$default$1"), - ProblemFilters.exclude[MissingTypesProblem]("org.scalajs.ir.Types$PrimRef$"), - - // constructor of a sealed abstract class, not an issue - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#PrimTypeWithRef.this"), - - // private, not an issue - ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Serializers$Deserializer$BodyHack5Transformer$"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Serializers#Hacks.use*"), ) val Linker = Seq( - // !!! Breaking, OK in minor release - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedClass.this"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedTopLevelExport.this"), + // private[linker], not an issue + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.CoreSpec.linkTimeProperties"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.linker.standard.LinkTimeProperties*"), ) val LinkerInterface = Seq( - // private, not an issue - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.interface.Semantics.this"), ) val SbtPlugin = Seq( @@ -64,10 +23,6 @@ object BinaryIncompatibilities { ) val Library = Seq( - // Changes covered by a deserialization hack (and the code cannot be used on the JVM, such as in macros) - ProblemFilters.exclude[AbstractClassProblem]("scala.scalajs.runtime.AnonFunction*"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.scalajs.runtime.AnonFunction*.this"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.scalajs.runtime.AnonFunction*.apply"), ) val TestInterface = Seq( diff --git a/project/Build.scala b/project/Build.scala index eb8b6b9f2f..2141d5f3b3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -205,15 +205,6 @@ object MyScalaJSPlugin extends AutoPlugin { } } }, - - /* The AppVeyor CI build definition is very sensitive to weird characthers - * in its command lines, so we cannot directly spell out the correct - * incantation. Instead, we define this alias. - */ - addCommandAlias( - "setSmallESModulesForAppVeyorCI", - "set testSuite.v2_12 / scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule).withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List(\"org.scalajs.testsuite\"))))" - ), ) override def projectSettings: Seq[Setting[_]] = Def.settings( @@ -396,7 +387,7 @@ object Build { "1.3.0", "1.3.1", "1.4.0", "1.5.0", "1.5.1", "1.6.0", "1.7.0", "1.7.1", "1.8.0", "1.9.0", "1.10.0", "1.10.1", "1.11.0", "1.12.0", "1.13.0", "1.13.1", "1.13.2", "1.14.0", "1.15.0", "1.16.0", "1.17.0", "1.18.0", - "1.18.1", "1.18.2") + "1.18.1", "1.18.2", "1.19.0") val previousVersion = previousVersions.last val previousBinaryCrossVersion = CrossVersion.binaryWith("sjs1_", "") @@ -759,9 +750,21 @@ object Build { val libFileMappings = (PathFinder(prevProducts) ** "*.sjsir") .pair(Path.rebase(prevProducts, outputDir)) + /* Note: we cannot use `linkerImpl` here to load `IRFile`s. That would + * create the circular dependency + * linkerPrivateLibrary/products + * -> linkerImpl + * -> linker/fullClasspath + * -> linkerPrivateLibrary/products + */ val dependencyFiles = { val cp = Attributed.data((internalDependencyClasspath in Compile).value) - (PathFinder(cp) ** "*.sjsir").get + cp.flatMap { entry => + if (entry.getName().endsWith(".jar")) + Seq(entry) + else + (PathFinder(entry) ** "*.sjsir").get + } } FileFunction.cached(s.cacheDirectory / "cleaned-sjsir", @@ -2050,16 +2053,16 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 624000 to 625000, - fullLink = 96000 to 97000, + fastLink = 625000 to 626000, + fullLink = 94000 to 95000, fastLinkGz = 75000 to 79000, - fullLinkGz = 25000 to 26000, + fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 424000 to 425000, - fullLink = 281000 to 282000, - fastLinkGz = 60000 to 61000, + fastLink = 425000 to 426000, + fullLink = 283000 to 284000, + fastLinkGz = 61000 to 62000, fullLinkGz = 43000 to 44000, )) } @@ -2067,15 +2070,15 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 442000 to 443000, - fullLink = 93000 to 94000, + fastLink = 443000 to 444000, + fullLink = 90000 to 91000, fastLinkGz = 57000 to 58000, - fullLinkGz = 25000 to 26000, + fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 299000 to 300000, - fullLink = 257000 to 258000, + fastLink = 301000 to 302000, + fullLink = 259000 to 260000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, )) @@ -2135,7 +2138,6 @@ object Build { List(sharedTestDir / "scala", sharedTestDir / "require-scala2") ::: collectionsEraDependentDirectory(scalaV, sharedTestDir) :: includeIf(sharedTestDir / "require-jdk11", javaV >= 11) ::: - includeIf(sharedTestDir / "require-jdk15", javaV >= 15) ::: includeIf(sharedTestDir / "require-jdk17", javaV >= 17) ::: includeIf(sharedTestDir / "require-jdk21", javaV >= 21) ::: includeIf(testDir / "require-scala2", isJSTest) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index be6103d72c..f2f6f6a046 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -10,10 +10,14 @@ import org.scalajs.ir.WellKnownNames._ import java.io._ import java.net.URI -import java.nio.file.Files +import java.nio._ +import java.nio.file._ +import java.nio.file.attribute._ +import java.util.stream.{Stream, StreamSupport} import scala.collection.immutable.IndexedSeq import scala.collection.mutable +import scala.collection.JavaConverters._ import sbt.{Logger, MessageOnlyException} @@ -41,6 +45,8 @@ import sbt.{Logger, MessageOnlyException} final class JavalibIRCleaner(baseDirectoryURI: URI) { import JavalibIRCleaner._ + type JSTypes = Map[ClassName, Option[JSNativeLoadSpec]] + def cleanIR(dependencyFiles: Seq[File], libFileMappings: Seq[(File, File)], logger: Logger): Set[File] = { @@ -53,9 +59,16 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } val jsTypes = { - val dependencyIR = dependencyFiles.iterator.map(readIR(_)) - val libIR = libIRMappings.iterator.map(_._1) - getJSTypes(dependencyIR ++ libIR) + val dependencyIR: Stream[ClassDef] = { + dependencyFiles.asJava.stream().flatMap { file => + if (file.getName().endsWith(".jar")) + readIRJar(file) + else + Stream.of[ClassDef](readIR(file)) + } + } + val libIR: Stream[ClassDef] = libIRMappings.asJava.stream().map(_._1) + getJSTypes(Stream.concat(dependencyIR, libIR)) } val resultBuilder = Set.newBuilder[File] @@ -107,14 +120,32 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { def errorCount: Int = _errorCount } - private def readIR(file: File): ClassDef = { - import java.nio.ByteBuffer + private def readIR(file: File): ClassDef = + readIR(file.toPath()) - val bytes = Files.readAllBytes(file.toPath()) + private def readIR(path: Path): ClassDef = { + val bytes = Files.readAllBytes(path) val buffer = ByteBuffer.wrap(bytes) Serializers.deserialize(buffer) } + private def readIRJar(jar: File): Stream[ClassDef] = { + def isIRFile(path: Path) = { + val fn = path.getFileName() // null if path is FS root + fn != null && fn.toString().endsWith(".sjsir") + } + + // Open zip/jar file as filesystem. + // The type ascription is necessary on JDK 13+. + val fs = FileSystems.newFileSystem(jar.toPath(), null: ClassLoader) + StreamSupport + .stream(fs.getRootDirectories().spliterator(), /* parallel= */ false) + .flatMap(Files.walk(_)) + .filter(isIRFile(_)) + .map[ClassDef](readIR(_)) + .onClose(() => fs.close()) // only close fs once all IR is read. + } + private def writeIRFile(file: File, tree: ClassDef): Unit = { Files.createDirectories(file.toPath().getParent()) val outputStream = @@ -126,17 +157,19 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } } - private def getJSTypes(trees: Iterator[ClassDef]): Map[ClassName, ClassDef] = - trees.filter(_.kind.isJSType).map(t => t.className -> t).toMap + private def getJSTypes(trees: Stream[ClassDef]): JSTypes = { + trees.filter(_.kind.isJSType).reduce[JSTypes]( + Map.empty, (m, v) => m.updated(v.className, v.jsNativeLoadSpec), _ ++ _) + } - private def cleanTree(tree: ClassDef, jsTypes: Map[ClassName, ClassDef], + private def cleanTree(tree: ClassDef, jsTypes: JSTypes, errorManager: ErrorManager): ClassDef = { new ClassDefCleaner(tree.className, jsTypes, errorManager) .cleanClassDef(tree) } private final class ClassDefCleaner(enclosingClassName: ClassName, - jsTypes: Map[ClassName, ClassDef], errorManager: ErrorManager) + jsTypes: JSTypes, errorManager: ErrorManager) extends Transformers.ClassTransformer { def cleanClassDef(tree: ClassDef): ClassDef = { @@ -465,16 +498,13 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { private def genLoadFromLoadSpecOf(className: ClassName)( implicit pos: Position): Tree = { jsTypes.get(className) match { - case Some(classDef) => - classDef.jsNativeLoadSpec match { - case Some(loadSpec) => - genLoadFromLoadSpec(loadSpec) - case None => - reportError( - s"${className.nameString} does not have a load spec " + - "(this shouldn't have happened at all; bug in the compiler?)") - JSGlobalRef("Object") - } + case Some(Some(loadSpec)) => + genLoadFromLoadSpec(loadSpec) + case Some(None) => + reportError( + s"${className.nameString} does not have a load spec " + + "(this shouldn't have happened at all; bug in the compiler?)") + JSGlobalRef("Object") case None => reportError(s"${className.nameString} is not a JS type") JSGlobalRef("Object") diff --git a/project/MiniLib.scala b/project/MiniLib.scala index 0d447e4e30..69f5a08ca9 100644 --- a/project/MiniLib.scala +++ b/project/MiniLib.scala @@ -22,8 +22,6 @@ object MiniLib { "Double", "String", - "FloatingPointBits", - "Throwable", "StackTrace", "Error", diff --git a/project/NodeJSEnvForcePolyfills.scala b/project/NodeJSEnvForcePolyfills.scala index 6859a3aa7b..899bd79c7d 100644 --- a/project/NodeJSEnvForcePolyfills.scala +++ b/project/NodeJSEnvForcePolyfills.scala @@ -66,6 +66,7 @@ final class NodeJSEnvForcePolyfills(esVersion: ESVersion, config: NodeJSEnv.Conf |delete global.Set; |delete global.Symbol; | + |delete global.DataView; |delete global.Int8Array; |delete global.Int16Array; |delete global.Int32Array; diff --git a/scalalib/overrides-2.12/scala/collection/immutable/Range.scala b/scalalib/overrides-2.12/scala/collection/immutable/Range.scala index e5f4287a76..c4d5f9cdf1 100644 --- a/scalalib/overrides-2.12/scala/collection/immutable/Range.scala +++ b/scalalib/overrides-2.12/scala/collection/immutable/Range.scala @@ -33,7 +33,7 @@ import scala.collection.parallel.immutable.ParRange * `init`) are also permitted on overfull ranges. * * @param start the start of this range. - * @param end the end of the range. For exclusive ranges, e.g. + * @param end the end of the range. For exclusive ranges, e.g. * `Range(0,3)` or `(0 until 3)`, this is one * step past the last one in the range. For inclusive * ranges, e.g. `Range.inclusive(0,3)` or `(0 to 3)`, @@ -67,41 +67,92 @@ extends scala.collection.AbstractSeq[Int] { override def par = new ParRange(this) - private def gap = end.toLong - start.toLong - private def isExact = gap % step == 0 - private def hasStub = isInclusive || !isExact - private def longLength = gap / step + ( if (hasStub) 1 else 0 ) - - // Check cannot be evaluated eagerly because we have a pattern where - // ranges are constructed like: "x to y by z" The "x to y" piece - // should not trigger an exception. So the calculation is delayed, - // which means it will not fail fast for those cases where failing was - // correct. override final val isEmpty = ( - (start > end && step > 0) - || (start < end && step < 0) - || (start == end && !isInclusive) + if (isInclusive) + (if (step >= 0) start > end else start < end) + else + (if (step >= 0) start >= end else start <= end) ) + if (step == 0) throw new IllegalArgumentException("step cannot be 0.") + + /** Number of elements in this range, if it is non-empty. + * + * If the range is empty, `numRangeElements` does not have a meaningful value. + * + * Otherwise, `numRangeElements` is interpreted in the range [1, 2^32], + * respecting modular arithmetics wrt. the unsigned interpretation. + * In other words, it is 0 if the mathematical value should be 2^32, and the + * standard unsigned int encoding of the mathematical value otherwise. + * + * This interpretation allows to represent all values with the correct + * modular arithmetics, which streamlines the usage sites. + */ private val numRangeElements: Int = { - if (step == 0) throw new IllegalArgumentException("step cannot be 0.") - else if (isEmpty) 0 - else { - val len = longLength - if (len > scala.Int.MaxValue) -1 - else len.toInt - } + val stepSign = step >> 31 // if (step >= 0) 0 else -1 + val gap = ((end - start) ^ stepSign) - stepSign // if (step >= 0) (end - start) else -(end - start) + val absStep = (step ^ stepSign) - stepSign // if (step >= 0) step else -step + + /* If `absStep` is a constant 1, `div` collapses to being an alias of + * `gap`. Then `absStep * div` also collapses to `gap` and therefore + * `absStep * div != gap` constant-folds to `false`. + * + * Since most ranges are exclusive, that makes `numRangeElements` an alias + * of `gap`. Moreover, for exclusive ranges with step 1 and start 0 (which + * are the common case), it makes it an alias of `end` and the entire + * computation goes away. + */ + val div = Integer.divideUnsigned(gap, absStep) + if (isInclusive || (absStep * div != gap)) div + 1 else div } - // This field has a sensible value only for non-empty ranges - private val lastElement = step match { - case 1 => if (isInclusive) end else end-1 - case -1 => if (isInclusive) end else end+1 - case _ => - val remainder = (gap % step).toInt - if (remainder != 0) end - remainder - else if (isInclusive) end - else end - step + /** Computes the element of this range after `n` steps from `start`. + * + * `n` is interpreted as an unsigned integer. + * + * If the mathematical result is not within this Range, the result won't + * make sense, but won't error out. + */ + @inline + private[this] def locationAfterN(n: Int): Int = { + /* If `step >= 0`, we interpret `step * n` as an unsigned multiplication, + * and the addition as a mixed `(signed, unsigned) -> signed` operation. + * With those interpretation, they do not overflow, assuming the + * mathematical result is within this Range. + * + * If `step < 0`, we should compute `start - (-step * n)`, with the + * multiplication also interpreted as unsigned, and the subtraction as + * mixed. Again, using those interpreatations, they do not overflow. + * But then modular arithmetics allow us to cancel out the two `-` signs, + * so we end up with the same formula. + */ + start + (step * n) + } + + /** Last element of this non-empty range. + * + * For empty ranges, this value is nonsensical. + */ + private[this] val lastElement: Int = { + /* Since we can assume the range is non-empty, `(numRangeElements - 1)` + * is a valid unsigned value in the full int range. The general formula is + * therefore `locationAfterN(numRangeElements - 1)`. + * + * We special-case 1 and -1 so that, in the happy path where `step` is a + * constant 1 or -1, and we only use `foreach`, we can entirely + * dead-code-eliminate `numRangeElements` and its computation. + * + * When `step` is not constant, it is probably 1 or -1 anyway, so the + * single branch should be predictably true. + * + * `step == 1 || step == -1` + * equiv `(step + 1 == 2) || (step + 1 == 0)` + * equiv `((step + 1) & ~2) == 0` + */ + if (((step + 1) & ~2) == 0) + (if (isInclusive) end else end - step) + else + locationAfterN(numRangeElements - 1) } /** The last element of this range. This method will return the correct value @@ -134,18 +185,31 @@ extends scala.collection.AbstractSeq[Int] def isInclusive = false override def size = length - override def length = if (numRangeElements < 0) fail() else numRangeElements + override def length: Int = + if (isEmpty) 0 + else if (numRangeElements > 0) numRangeElements + else fail() + + // Check cannot be evaluated eagerly because we have a pattern where + // ranges are constructed like: "x to y by z" The "x to y" piece + // should not trigger an exception. So the calculation is delayed, + // which means it will not fail fast for those cases where failing was + // correct. private def fail() = Range.fail(start, end, step, isInclusive) private def validateMaxLength() { - if (numRangeElements < 0) + if (numRangeElements <= 0 && !isEmpty) fail() } final def apply(idx: Int): Int = { - validateMaxLength() - if (idx < 0 || idx >= numRangeElements) throw new IndexOutOfBoundsException(idx.toString) - else start + (step * idx) + /* If length is not valid, numRangeElements <= 0, so the condition is always true. + * We push validateMaxLength() inside the then branch, out of the happy path. + */ + if (idx < 0 || idx >= numRangeElements || isEmpty) { + validateMaxLength() + throw new IndexOutOfBoundsException(idx.toString) + } else locationAfterN(idx) } @inline final override def foreach[@specialized(Unit) U](f: Int => U) { @@ -161,6 +225,14 @@ extends scala.collection.AbstractSeq[Int] } } + /** Is the non-negative value `n` greater or equal to the number of elements + * in this non-empty range? + * + * This method returns nonsensical results if `n < 0` or if `this.isEmpty`. + */ + @inline private[this] def greaterEqualNumRangeElements(n: Int): Boolean = + (n ^ Int.MinValue) > ((numRangeElements - 1) ^ Int.MinValue) // unsigned comparison + /** Creates a new range containing the first `n` elements of this range. * * $doesNotUseBuilders @@ -168,15 +240,11 @@ extends scala.collection.AbstractSeq[Int] * @param n the number of elements to take. * @return a new range consisting of `n` first elements. */ - final override def take(n: Int): Range = ( + final override def take(n: Int): Range = { if (n <= 0 || isEmpty) newEmptyRange(start) - else if (n >= numRangeElements && numRangeElements >= 0) this - else { - // May have more than Int.MaxValue elements in range (numRangeElements < 0) - // but the logic is the same either way: take the first n - new Range.Inclusive(start, locationAfterN(n - 1), step) - } - ) + else if (greaterEqualNumRangeElements(n)) this + else new Range.Inclusive(start, locationAfterN(n - 1), step) + } /** Creates a new range containing all the elements of this range except the first `n` elements. * @@ -185,16 +253,12 @@ extends scala.collection.AbstractSeq[Int] * @param n the number of elements to drop. * @return a new range consisting of all the elements of this range except `n` first elements. */ - final override def drop(n: Int): Range = ( + final override def drop(n: Int): Range = { if (n <= 0 || isEmpty) this - else if (n >= numRangeElements && numRangeElements >= 0) newEmptyRange(end) - else { - // May have more than Int.MaxValue elements (numRangeElements < 0) - // but the logic is the same either way: go forwards n steps, keep the rest - copy(locationAfterN(n), end, step) - } - ) - + else if (greaterEqualNumRangeElements(n)) newEmptyRange(end) + else copy(locationAfterN(n), end, step) + } + /** Creates a new range containing the elements starting at `from` up to but not including `until`. * * $doesNotUseBuilders @@ -203,15 +267,17 @@ extends scala.collection.AbstractSeq[Int] * @param until the element at which to end (not included in the range) * @return a new range consisting of a contiguous interval of values in the old range */ - override def slice(from: Int, until: Int): Range = - if (from <= 0) take(until) - else if (until >= numRangeElements && numRangeElements >= 0) drop(from) + override def slice(from: Int, until: Int): Range = { + if (isEmpty) this + else if (from <= 0) take(until) + else if (greaterEqualNumRangeElements(until) && until >= 0) drop(from) else { val fromValue = locationAfterN(from) if (from >= until) newEmptyRange(fromValue) else new Range.Inclusive(fromValue, locationAfterN(until-1), step) } - + } + /** Creates a new range containing all the elements of this range except the last one. * * $doesNotUseBuilders @@ -249,9 +315,6 @@ extends scala.collection.AbstractSeq[Int] else current.toLong + step } } - // Methods like apply throw exceptions on invalid n, but methods like take/drop - // are forgiving: therefore the checks are with the methods. - private def locationAfterN(n: Int) = start + (step * n) // When one drops everything. Can't ever have unchecked operations // like "end + 1" or "end - 1" because ranges involving Int.{ MinValue, MaxValue } @@ -299,15 +362,9 @@ extends scala.collection.AbstractSeq[Int] * $doesNotUseBuilders */ final override def takeRight(n: Int): Range = { - if (n <= 0) newEmptyRange(start) - else if (numRangeElements >= 0) drop(numRangeElements - n) - else { - // Need to handle over-full range separately - val y = last - val x = y - step.toLong*(n-1) - if ((step > 0 && x < start) || (step < 0 && x > start)) this - else new Range.Inclusive(x.toInt, y, step) - } + if (n <= 0 || isEmpty) newEmptyRange(start) + else if (greaterEqualNumRangeElements(n)) this + else copy(locationAfterN(numRangeElements - n), end, step) } /** Creates a new range consisting of the initial `length - n` elements of the range. @@ -315,14 +372,9 @@ extends scala.collection.AbstractSeq[Int] * $doesNotUseBuilders */ final override def dropRight(n: Int): Range = { - if (n <= 0) this - else if (numRangeElements >= 0) take(numRangeElements - n) - else { - // Need to handle over-full range separately - val y = last - step.toInt*n - if ((step > 0 && y < start) || (step < 0 && y > start)) newEmptyRange(start) - else new Range.Inclusive(start, y.toInt, step) - } + if (n <= 0 || isEmpty) this + else if (greaterEqualNumRangeElements(n)) newEmptyRange(end) + else new Range.Inclusive(start, locationAfterN(numRangeElements - 1 - n), step) } /** Returns the reverse of this range. @@ -340,14 +392,14 @@ extends scala.collection.AbstractSeq[Int] else new Range.Inclusive(start, end, step) final def contains(x: Int) = { - if (x==end && !isInclusive) false + if (isEmpty) false else if (step > 0) { - if (x < start || x > end) false - else (step == 1) || (((x - start) % step) == 0) + if (x < start || x > lastElement) false + else (step == 1) || (Integer.remainderUnsigned(x - start, step) == 0) } else { - if (x < end || x > start) false - else (step == -1) || (((x - start) % step) == 0) + if (x > start || x < lastElement) false + else (step == -1) || (Integer.remainderUnsigned(start - x, -step) == 0) } } @@ -398,8 +450,13 @@ extends scala.collection.AbstractSeq[Int] override def toString = { val preposition = if (isInclusive) "to" else "until" + val stepped = if (step == 1) "" else s" by $step" - val prefix = if (isEmpty) "empty " else if (!isExact) "inexact " else "" + def isInexact = + if (isInclusive) lastElement != end + else (lastElement + step) != end + + val prefix = if (isEmpty) "empty " else if (isInexact) "inexact " else "" s"${prefix}Range $start $preposition $end$stepped" } } @@ -432,16 +489,19 @@ object Range { ) if (isEmpty) 0 else { - // Counts with Longs so we can recognize too-large ranges. - val gap: Long = end.toLong - start.toLong - val jumps: Long = gap / step - // Whether the size of this range is one larger than the - // number of full-sized jumps. - val hasStub = isInclusive || (gap % step != 0) - val result: Long = jumps + ( if (hasStub) 1 else 0 ) - - if (result > scala.Int.MaxValue) -1 - else result.toInt + val stepSign = step >> 31 // if (step >= 0) 0 else -1 + val gap = ((end - start) ^ stepSign) - stepSign // if (step >= 0) (end - start) else -(end - start) + val absStep = (step ^ stepSign) - stepSign // if (step >= 0) step else -step + + val div = Integer.divideUnsigned(gap, absStep) + if (isInclusive) { + if (div == -1) // max unsigned int + -1 // corner case: there are 2^32 elements, which would overflow to 0 + else + div + 1 + } else { + if (absStep * div != gap) div + 1 else div + } } } def count(start: Int, end: Int, step: Int): Int = diff --git a/scalalib/overrides-2.13/scala/collection/immutable/Range.scala b/scalalib/overrides-2.13/scala/collection/immutable/Range.scala index 310c45c079..6e3130a886 100644 --- a/scalalib/overrides-2.13/scala/collection/immutable/Range.scala +++ b/scalalib/overrides-2.13/scala/collection/immutable/Range.scala @@ -81,40 +81,99 @@ sealed abstract class Range( r.asInstanceOf[S with EfficientSplit] } - private[this] def gap = end.toLong - start.toLong - private[this] def isExact = gap % step == 0 - private[this] def hasStub = isInclusive || !isExact - private[this] def longLength = gap / step + ( if (hasStub) 1 else 0 ) - def isInclusive: Boolean final override val isEmpty: Boolean = ( - (start > end && step > 0) - || (start < end && step < 0) - || (start == end && !isInclusive) - ) - + if (isInclusive) + (if (step >= 0) start > end else start < end) + else + (if (step >= 0) start >= end else start <= end) + ) + + if (step == 0) throw new IllegalArgumentException("step cannot be 0.") + + /** Number of elements in this range, if it is non-empty. + * + * If the range is empty, `numRangeElements` does not have a meaningful value. + * + * Otherwise, `numRangeElements` is interpreted in the range [1, 2^32], + * respecting modular arithmetics wrt. the unsigned interpretation. + * In other words, it is 0 if the mathematical value should be 2^32, and the + * standard unsigned int encoding of the mathematical value otherwise. + * + * This interpretation allows to represent all values with the correct + * modular arithmetics, which streamlines the usage sites. + */ private[this] val numRangeElements: Int = { - if (step == 0) throw new IllegalArgumentException("step cannot be 0.") - else if (isEmpty) 0 - else { - val len = longLength - if (len > scala.Int.MaxValue) -1 - else len.toInt - } + val stepSign = step >> 31 // if (step >= 0) 0 else -1 + val gap = ((end - start) ^ stepSign) - stepSign // if (step >= 0) (end - start) else -(end - start) + val absStep = (step ^ stepSign) - stepSign // if (step >= 0) step else -step + + /* If `absStep` is a constant 1, `div` collapses to being an alias of + * `gap`. Then `absStep * div` also collapses to `gap` and therefore + * `absStep * div != gap` constant-folds to `false`. + * + * Since most ranges are exclusive, that makes `numRangeElements` an alias + * of `gap`. Moreover, for exclusive ranges with step 1 and start 0 (which + * are the common case), it makes it an alias of `end` and the entire + * computation goes away. + */ + val div = Integer.divideUnsigned(gap, absStep) + if (isInclusive || (absStep * div != gap)) div + 1 else div } - final def length = if (numRangeElements < 0) fail() else numRangeElements + final def length: Int = + if (isEmpty) 0 + else if (numRangeElements > 0) numRangeElements + else fail() + + /** Computes the element of this range after `n` steps from `start`. + * + * `n` is interpreted as an unsigned integer. + * + * If the mathematical result is not within this Range, the result won't + * make sense, but won't error out. + */ + @inline + private[this] def locationAfterN(n: Int): Int = { + /* If `step >= 0`, we interpret `step * n` as an unsigned multiplication, + * and the addition as a mixed `(signed, unsigned) -> signed` operation. + * With those interpretation, they do not overflow, assuming the + * mathematical result is within this Range. + * + * If `step < 0`, we should compute `start - (-step * n)`, with the + * multiplication also interpreted as unsigned, and the subtraction as + * mixed. Again, using those interpreatations, they do not overflow. + * But then modular arithmetics allow us to cancel out the two `-` signs, + * so we end up with the same formula. + */ + start + (step * n) + } - // This field has a sensible value only for non-empty ranges - private[this] val lastElement = step match { - case 1 => if (isInclusive) end else end-1 - case -1 => if (isInclusive) end else end+1 - case _ => - val remainder = (gap % step).toInt - if (remainder != 0) end - remainder - else if (isInclusive) end - else end - step + /** Last element of this non-empty range. + * + * For empty ranges, this value is nonsensical. + */ + private[this] val lastElement: Int = { + /* Since we can assume the range is non-empty, `(numRangeElements - 1)` + * is a valid unsigned value in the full int range. The general formula is + * therefore `locationAfterN(numRangeElements - 1)`. + * + * We special-case 1 and -1 so that, in the happy path where `step` is a + * constant 1 or -1, and we only use `foreach`, we can entirely + * dead-code-eliminate `numRangeElements` and its computation. + * + * When `step` is not constant, it is probably 1 or -1 anyway, so the + * single branch should be predictably true. + * + * `step == 1 || step == -1` + * equiv `(step + 1 == 2) || (step + 1 == 0)` + * equiv `((step + 1) & ~2) == 0` + */ + if (((step + 1) & ~2) == 0) + (if (isInclusive) end else end - step) + else + locationAfterN(numRangeElements - 1) } /** The last element of this range. This method will return the correct value @@ -168,16 +227,21 @@ sealed abstract class Range( // which means it will not fail fast for those cases where failing was // correct. private[this] def validateMaxLength(): Unit = { - if (numRangeElements < 0) + if (numRangeElements <= 0 && !isEmpty) fail() } private[this] def fail() = Range.fail(start, end, step, isInclusive) @throws[IndexOutOfBoundsException] final def apply(idx: Int): Int = { - validateMaxLength() - if (idx < 0 || idx >= numRangeElements) throw new IndexOutOfBoundsException(s"$idx is out of bounds (min 0, max ${numRangeElements-1})") - else start + (step * idx) + /* If length is not valid, numRangeElements <= 0, so the condition is always true. + * We push validateMaxLength() inside the then branch, out of the happy path. + */ + if (idx < 0 || idx >= numRangeElements || isEmpty) { + validateMaxLength() + val max = if (isEmpty) -1 else numRangeElements - 1 + throw new IndexOutOfBoundsException(s"$idx is out of bounds (min 0, max $max)") + } else locationAfterN(idx) } /*@`inline`*/ final override def foreach[@specialized(Unit) U](f: Int => U): Unit = { @@ -225,48 +289,44 @@ sealed abstract class Range( case _ => super.sameElements(that) } + /** Is the non-negative value `n` greater or equal to the number of elements + * in this non-empty range? + * + * This method returns nonsensical results if `n < 0` or if `this.isEmpty`. + */ + @inline private[this] def greaterEqualNumRangeElements(n: Int): Boolean = + (n ^ Int.MinValue) > ((numRangeElements - 1) ^ Int.MinValue) // unsigned comparison + /** Creates a new range containing the first `n` elements of this range. * * @param n the number of elements to take. * @return a new range consisting of `n` first elements. */ - final override def take(n: Int): Range = + final override def take(n: Int): Range = { if (n <= 0 || isEmpty) newEmptyRange(start) - else if (n >= numRangeElements && numRangeElements >= 0) this - else { - // May have more than Int.MaxValue elements in range (numRangeElements < 0) - // but the logic is the same either way: take the first n - new Range.Inclusive(start, locationAfterN(n - 1), step) - } + else if (greaterEqualNumRangeElements(n)) this + else new Range.Inclusive(start, locationAfterN(n - 1), step) + } /** Creates a new range containing all the elements of this range except the first `n` elements. * * @param n the number of elements to drop. * @return a new range consisting of all the elements of this range except `n` first elements. */ - final override def drop(n: Int): Range = + final override def drop(n: Int): Range = { if (n <= 0 || isEmpty) this - else if (n >= numRangeElements && numRangeElements >= 0) newEmptyRange(end) - else { - // May have more than Int.MaxValue elements (numRangeElements < 0) - // but the logic is the same either way: go forwards n steps, keep the rest - copy(locationAfterN(n), end, step) - } + else if (greaterEqualNumRangeElements(n)) newEmptyRange(end) + else copy(locationAfterN(n), end, step) + } /** Creates a new range consisting of the last `n` elements of the range. * * $doesNotUseBuilders */ final override def takeRight(n: Int): Range = { - if (n <= 0) newEmptyRange(start) - else if (numRangeElements >= 0) drop(numRangeElements - n) - else { - // Need to handle over-full range separately - val y = last - val x = y - step.toLong*(n-1) - if ((step > 0 && x < start) || (step < 0 && x > start)) this - else Range.inclusive(x.toInt, y, step) - } + if (n <= 0 || isEmpty) newEmptyRange(start) + else if (greaterEqualNumRangeElements(n)) this + else copy(locationAfterN(numRangeElements - n), end, step) } /** Creates a new range consisting of the initial `length - n` elements of the range. @@ -274,14 +334,9 @@ sealed abstract class Range( * $doesNotUseBuilders */ final override def dropRight(n: Int): Range = { - if (n <= 0) this - else if (numRangeElements >= 0) take(numRangeElements - n) - else { - // Need to handle over-full range separately - val y = last - step.toInt*n - if ((step > 0 && y < start) || (step < 0 && y > start)) newEmptyRange(start) - else Range.inclusive(start, y.toInt, step) - } + if (n <= 0 || isEmpty) this + else if (greaterEqualNumRangeElements(n)) newEmptyRange(end) + else new Range.Inclusive(start, locationAfterN(numRangeElements - 1 - n), step) } // Advance from the start while we meet the given test @@ -334,22 +389,20 @@ sealed abstract class Range( * @param until the element at which to end (not included in the range) * @return a new range consisting of a contiguous interval of values in the old range */ - final override def slice(from: Int, until: Int): Range = - if (from <= 0) take(until) - else if (until >= numRangeElements && numRangeElements >= 0) drop(from) + final override def slice(from: Int, until: Int): Range = { + if (isEmpty) this + else if (from <= 0) take(until) + else if (greaterEqualNumRangeElements(until) && until >= 0) drop(from) else { val fromValue = locationAfterN(from) if (from >= until) newEmptyRange(fromValue) - else Range.inclusive(fromValue, locationAfterN(until-1), step) + else new Range.Inclusive(fromValue, locationAfterN(until-1), step) } + } // Overridden only to refine the return type final override def splitAt(n: Int): (Range, Range) = (take(n), drop(n)) - // Methods like apply throw exceptions on invalid n, but methods like take/drop - // are forgiving: therefore the checks are with the methods. - private[this] def locationAfterN(n: Int) = start + (step * n) - // When one drops everything. Can't ever have unchecked operations // like "end + 1" or "end - 1" because ranges involving Int.{ MinValue, MaxValue } // will overflow. This creates an exclusive range where start == end @@ -369,13 +422,13 @@ sealed abstract class Range( else new Range.Inclusive(start, end, step) final def contains(x: Int) = { - if (x == end && !isInclusive) false + if (isEmpty) false else if (step > 0) { - if (x < start || x > end) false + if (x < start || x > lastElement) false else (step == 1) || (Integer.remainderUnsigned(x - start, step) == 0) } else { - if (x < end || x > start) false + if (x > start || x < lastElement) false else (step == -1) || (Integer.remainderUnsigned(start - x, -step) == 0) } } @@ -478,7 +531,12 @@ sealed abstract class Range( final override def toString: String = { val preposition = if (isInclusive) "to" else "until" val stepped = if (step == 1) "" else s" by $step" - val prefix = if (isEmpty) "empty " else if (!isExact) "inexact " else "" + + def isInexact = + if (isInclusive) lastElement != end + else (lastElement + step) != end + + val prefix = if (isEmpty) "empty " else if (isInexact) "inexact " else "" s"${prefix}Range $start $preposition $end$stepped" } @@ -549,16 +607,19 @@ object Range { if (isEmpty) 0 else { - // Counts with Longs so we can recognize too-large ranges. - val gap: Long = end.toLong - start.toLong - val jumps: Long = gap / step - // Whether the size of this range is one larger than the - // number of full-sized jumps. - val hasStub = isInclusive || (gap % step != 0) - val result: Long = jumps + ( if (hasStub) 1 else 0 ) - - if (result > scala.Int.MaxValue) -1 - else result.toInt + val stepSign = step >> 31 // if (step >= 0) 0 else -1 + val gap = ((end - start) ^ stepSign) - stepSign // if (step >= 0) (end - start) else -(end - start) + val absStep = (step ^ stepSign) - stepSign // if (step >= 0) step else -step + + val div = Integer.divideUnsigned(gap, absStep) + if (isInclusive) { + if (div == -1) // max unsigned int + -1 // corner case: there are 2^32 elements, which would overflow to 0 + else + div + 1 + } else { + if (absStep * div != gap) div + 1 else div + } } } def count(start: Int, end: Int, step: Int): Int = diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/LinkTimeIfTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/LinkTimeIfTest.scala new file mode 100644 index 0000000000..1cca641fbf --- /dev/null +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/LinkTimeIfTest.scala @@ -0,0 +1,95 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.library + +import scala.scalajs.js +import scala.scalajs.LinkingInfo._ + +import org.junit.Test +import org.junit.Assert._ +import org.junit.Assume._ + +import org.scalajs.testsuite.utils.Platform + +class LinkTimeIfTest { + @Test def linkTimeIfConst(): Unit = { + // boolean const + assertEquals(1, linkTimeIf(true) { 1 } { 2 }) + assertEquals(2, linkTimeIf(false) { 1 } { 2 }) + } + + @Test def linkTimeIfProp(): Unit = { + locally { + val cond = Platform.isInProductionMode + assertEquals(cond, linkTimeIf(productionMode) { true } { false }) + } + + locally { + val cond = !Platform.isInProductionMode + assertEquals(cond, linkTimeIf(!productionMode) { true } { false }) + } + } + + @Test def linkTimIfIntProp(): Unit = { + locally { + val cond = Platform.assumedESVersion >= ESVersion.ES2015 + assertEquals(cond, linkTimeIf(esVersion >= ESVersion.ES2015) { true } { false }) + } + + locally { + val cond = !(Platform.assumedESVersion < ESVersion.ES2015) + assertEquals(cond, linkTimeIf(!(esVersion < ESVersion.ES2015)) { true } { false }) + } + } + + @Test def linkTimeIfNested(): Unit = { + locally { + val cond = { + Platform.isInProductionMode && + Platform.assumedESVersion >= ESVersion.ES2015 + } + assertEquals(if (cond) 53 else 78, + linkTimeIf(productionMode && esVersion >= ESVersion.ES2015) { 53 } { 78 }) + } + + locally { + val cond = { + Platform.assumedESVersion >= ESVersion.ES2015 && + Platform.assumedESVersion < ESVersion.ES2019 && + Platform.isInProductionMode + } + val result = linkTimeIf(esVersion >= ESVersion.ES2015 && + esVersion < ESVersion.ES2019 && productionMode) { + 53 + } { + 78 + } + assertEquals(if (cond) 53 else 78, result) + } + } + + @Test def exponentOp(): Unit = { + def pow(x: Double, y: Double): Double = { + linkTimeIf(esVersion >= ESVersion.ES2016) { + assertTrue("Took the wrong branch of linkTimeIf when linking for ES 2016+", + esVersion >= ESVersion.ES2016) + (x.asInstanceOf[js.Dynamic] ** y.asInstanceOf[js.Dynamic]).asInstanceOf[Double] + } { + assertFalse("Took the wrong branch of linkTimeIf when linking for ES 2015-", + esVersion >= ESVersion.ES2016) + Math.pow(x, y) + } + } + assertEquals(pow(2.0, 8.0), 256.0, 0) + } +} diff --git a/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/lang/MathTestOnJDK11.scala b/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/lang/MathTestOnJDK11.scala new file mode 100644 index 0000000000..a94c198e9e --- /dev/null +++ b/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/lang/MathTestOnJDK11.scala @@ -0,0 +1,242 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.javalib.lang + +import java.math.BigInteger +import java.util.SplittableRandom + +import org.junit.Test +import org.junit.Assert._ + +class MathTestOnJDK11 { + + @noinline + private def hideFromOptimizer(x: Int): Int = x + + @Test def testMultiplyFull(): Unit = { + @inline def test(expected: Long, x: Int, y: Int): Unit = { + assertEquals(expected, Math.multiplyFull(x, y)) + assertEquals(expected, Math.multiplyFull(x, hideFromOptimizer(y))) + assertEquals(expected, Math.multiplyFull(hideFromOptimizer(x), y)) + assertEquals(expected, Math.multiplyFull(hideFromOptimizer(x), hideFromOptimizer(y))) + } + + test(2641928036408725662L, 1942041231, 1360387202) + test(54843908448922272L, 1565939409, 35023008) + test(510471553407128558L, 1283300489, 397780222) + test(-1211162085735907941L, -1990140693, 608581137) + test(-1197265696701533712L, -584098468, 2049766884) + test(203152587796496856L, -1809591416, -112264341) + test(-1869763755321108598L, 1235591906, -1513253483) + test(-737954189546644064L, 675415792, -1092592442) + test(-2570904460570261986L, 1639253754, -1568338309) + test(1106623967126000400L, 2088029790, 529984760) + test(1407516248272451352L, -869881054, -1618055988) + test(-2120367337662071940L, -1558894530, 1360173698) + test(-1464086284066637244L, -1417313902, 1033000722) + test(36729253163312334L, -1673852034, -21942951) + test(-3197007331876781046L, 1876799847, -1703435418) + test(461794994386945009L, -246001091, -1877207099) + test(-1206231192496917804L, 867896526, -1389832954) + test(-1739671893103255929L, -1083992841, 1604873969) + test(-409626127116780624L, 240101424, -1706054551) + test(-3083566560548370936L, -1568530113, 1965895672) + test(-1205028798380605000L, -1201743532, 1002733750) + test(-1328689065035027168L, 929349664, -1429697687) + test(-124212693522020684L, 80893862, -1535502082) + test(-82341860111074830L, -243230690, 338534007) + test(-846837059701860202L, 1959770926, -432110227) + test(335728245390354432L, 506816728, 662425344) + test(745294755971022170L, 1521993302, 489683335) + test(-2370525755201631608L, 2023520366, -1171485988) + test(-1039854583047715776L, 593162592, -1753068378) + test(-152985384388127808L, -635946432, 240563319) + test(-678107568956539050L, 649113254, -1044667575) + test(-3064094283703186444L, -1890896836, 1620444979) + test(1240687269228318870L, -1080325230, -1148438669) + test(-46551523496333580L, 27167878, -1713476610) + test(-2500430606368427103L, 2023288183, -1235825241) + test(92963399778762084L, 896198732, 103730787) + test(2469065794894324667L, 2105111101, 1172890967) + test(172558569988357136L, -142945148, -1207166332) + test(335684786634110970L, -1647598405, -203741874) + test(2406859843746696240L, 2049365815, 1174441296) + test(3100973294006114952L, 1991928152, 1556769651) + test(-335912134649077352L, 866240524, -387781598) + test(84303320581066207L, 75666091, 1114149277) + test(-2623126349572207976L, 1426933667, -1838295928) + test(59139945163750590L, 149344270, 395997417) + test(-105764175098643999L, 68726447, -1538915217) + test(8595303129864000L, 726092025, 11837760) + test(-2958527843471399088L, 1536412078, -1925608296) + test(1532625839159904477L, 867021537, 1767690621) + test(384402376484481316L, 1207235521, 318415396) + test(-219376614576542698L, 1816299166, -120782203) + test(-672138807810988440L, 531516745, -1264567512) + test(-193351903065245331L, 170858169, -1131651499) + test(71263251057597648L, 51058196, 1395725988) + test(-774312974742971385L, 1958551603, -395349795) + test(-1846593638370672048L, 1190143097, -1551572784) + test(240083094242536384L, 1404614968, 170924488) + test(-130950827889833280L, -115480554, 1133964320) + test(128954457719585228L, 735993884, 175211317) + test(364779990580792000L, -668489125, -545678272) + test(107252402494512045L, 759517757, 141211185) + test(3038084150893069044L, -1924640913, -1578519988) + test(760804294233336624L, -728394552, -1044494762) + test(1171051779605774913L, 848233701, 1380576813) + test(-1805862307837393080L, -1385644986, 1303264780) + test(172227703288618734L, -104999826, -1640266559) + test(150448013961014407L, 163398103, 920745169) + test(-671469201380991232L, 650262784, -1032612073) + test(-1325861126942924945L, -1773644581, 747534845) + test(987406376890116568L, -1626507773, -607071416) + test(2918138947401192144L, 1695881208, 1720721318) + test(-2590993826910153940L, -1397240042, 1854365570) + test(954644624447419276L, -1516139806, -629654746) + test(407510452326678620L, -384747652, -1059162935) + test(149866317537821404L, 1530355444, 97929091) + test(922044716091910632L, 968149268, 952378674) + test(-3508732521573808284L, 1825364562, -1922209182) + test(1701723136959404304L, 894776752, 1901841027) + test(-2435876799625512705L, -1276062909, 1908900245) + test(-516933170985379201L, 657063047, -786732983) + test(123334479976750576L, 313765817, 393078128) + test(-1072624004420456775L, -894199299, 1199535725) + test(301682711612188737L, 330918981, 911651277) + test(1790992996470651507L, -1115945231, -1604911197) + test(-2750453268538140155L, 1878389719, -1464261245) + test(758285757353272504L, 1259684942, 601964612) + test(-218581674312137400L, -161533394, 1353167100) + test(-1824007072461951836L, -1244277844, 1465916219) + test(-92753167730460334L, -65368843, 1418920138) + test(-2326636630979491248L, 1124395877, -2069232624) + test(-7380586257943446L, 29715454, -248375349) + test(31319707234597638L, 491995506, 63658523) + test(-1196559502630778250L, -1752963990, 682592175) + test(166065559841839548L, -911521074, -182185102) + test(-1222260378510810100L, 1071539812, -1140657925) + test(57800571165871464L, -257569032, -224408077) + test(332444627169725608L, 1247224172, 266547614) + test(217903869180130650L, 1069161915, 203808110) + test(920425054266935850L, -901689546, -1020778225) + test(-507632632656614388L, 864632142, -587108214) + } + + @Test def testMultiplyHigh(): Unit = { + def test(expected: Long, x: Long, y: Long): Unit = + assertEquals(expected, Math.multiplyHigh(x, y)) + + test(-2514789262281153376L, 8217931296694472096L, -5644933286224084859L) + test(-298247406641127011L, -8034902747807161194L, 684724352445702293L) + test(242644198957550459L, 717019025263929004L, 6242505821226454837L) + test(-1089698470915011537L, -7558081430876177893L, 2659588811568490384L) + test(138675986327040026L, 2362930226177876193L, 1082605148727562445L) + test(-1260260349245855816L, -3350308785473442797L, 6938972380570262589L) + test(-1799534229489533301L, -4097805274432763180L, 8100811327075225922L) + test(437623091041087696L, -2968271773754119013L, -2719670493975918294L) + test(-107841114219899514L, 2013609532543228156L, -987936043452088475L) + test(2757621741022067854L, -7005993850636185311L, -7260803191272031988L) + test(-187671345159116030L, 1781219534362173574L, -1943570237881252419L) + test(-515018730942796014L, 6085558843030314089L, -1561141543105626636L) + test(-119091959391883575L, 7423442237814967910L, -295935339127164155L) + test(18351865713513547L, -1886460125362775846L, -179453657960126825L) + test(3928100041033091765L, 8449838094261471293L, 8575389888485029447L) + test(-7404756889594137L, -89549316594063561L, 1525345591296625693L) + test(714591873345926311L, -2929853068304815970L, -4499165349746322236L) + test(1305977852854305585L, -5568549492657237090L, -4326268312655360053L) + test(-2435010516398991446L, 6443930667478151719L, -6970592660082469124L) + test(2031324595328562735L, 5390460907312723801L, 6951413911530987604L) + test(34713245667458599L, -535353692461820541L, -1196118319182197181L) + test(255381044848343425L, -3176530727082196631L, -1483048388428836603L) + test(6566871520624982L, -33326351213089011L, -3634883324950494373L) + test(156130078476475485L, 687410849583778615L, 4189767446364284457L) + test(1647679448547038188L, 4460502251200507739L, 6814102850116870938L) + test(-2241611115434343963L, 5633894511267143863L, -7339581257068946568L) + test(-93572860194426351L, -1075368508503119813L, 1605137764964203383L) + test(1663347345126188661L, -6330756750592024018L, -4846710115399342760L) + test(-1686630202076061136L, 5124142056960069542L, -6071813649745693328L) + test(728105493712673843L, -8079843401135830331L, -1662306437683128283L) + test(-2030727779883712688L, 4452689522888653156L, -8412963770845872378L) + test(734253555387491804L, 5835084770836409518L, 2321232330529258387L) + test(2018627311798804222L, -7211950082779933827L, -5163250018863045382L) + test(-1244560006523295051L, -7326211205612788508L, 3133690700470219958L) + test(-492070935033321215L, 1614944457187625808L, -5620692751550184667L) + test(319340972880203566L, 2310036532484690677L, 2550090059672932009L) + test(1766280783448332865L, 5949345770128658249L, 5476590340096838859L) + test(2757208297958468913L, -5707089944199929572L, -8911987777945981523L) + test(408328069441815717L, 1242541635079749093L, 6062028975489127199L) + test(-77985829287979398L, -7943526433115400350L, 181101510313367840L) + test(-230121117022373017L, -780391911062895469L, 5439555807140802418L) + test(2588662639521587653L, 7451684432618227097L, 6408268846625040081L) + test(861249002493118404L, 1744344496585548181L, 9107856827493957233L) + test(-2703044944335540474L, 8052570526613861366L, -6192106997771248181L) + test(-2975059248415970510L, 6503508572335523474L, -8438546047759521035L) + test(-370291189062632935L, -8722964233277178137L, 783067156383574516L) + test(-90473002639507852L, 852694261922564555L, -1957245873225555126L) + test(-218977334338454381L, -1819563432425194345L, 2219993418476586419L) + test(-1087231185918604076L, -2941838679159182506L, 6817462690146034563L) + test(-1170480051005916145L, -2771463765488827700L, 7790665067735548924L) + test(-371145713487913188L, 3224241917397787909L, -2123423169279885562L) + test(-502492608136209963L, 1568228348895174267L, -5910716094215359887L) + test(1445926343733049503L, -7706328512722939071L, -3461133686196008644L) + test(-1374053009197983052L, -8787832166727089323L, 2884306814637966447L) + test(-1910150305525172307L, 8663815092401732543L, -4067036686787486282L) + test(2074971709256543740L, 8092193156887080609L, 4730049238662438083L) + test(953725989108917020L, 8492699833366153401L, 2071560232049848145L) + test(334989155711573307L, 1093268576921704206L, 5652279186765632978L) + test(129011196343964709L, 1000276763122669782L, 2379178052852915387L) + test(239042793587178901L, 3208737625070847213L, 1374235525371105170L) + test(127809344420152430L, -7696730067895344868L, -306320508313194466L) + test(-2506455997163955037L, -5731747797284935902L, 8066641092198683254L) + test(3016086034985660469L, -6992699346126002928L, -7956436339922591224L) + test(-1527917483534567268L, -8938885845855254814L, 3153089016969294968L) + test(-1268939936756528050L, 5537112727075101653L, -4227439716695399205L) + test(-37535014067603004L, -8605247800544091240L, 80462389271855887L) + test(-2710920384572235679L, -7926242046619125682L, 6309125338878172023L) + test(-3331830886924716794L, 6823617049086893513L, -9007163096323738999L) + test(1854911433578401793L, -4644835313936852982L, -7366693150982113934L) + test(-3840461794042836575L, 8006480391435326631L, -8848334396141248546L) + test(-1212641710132993432L, -7017377545321262459L, 3187699555205380404L) + test(946047090630044138L, -5829622550331878687L, -2993588077419595837L) + test(3518955178043574292L, -7909090733489625033L, -8207424565425867851L) + test(1231895337081111773L, 2841977238766797132L, 7996002817598962425L) + test(-1649686524869089287L, -3558405071306300052L, 8551962049372852642L) + test(1156466789444347220L, -8077807627762096372L, -2640945152160624636L) + test(-284428196958678125L, 7604654143237097972L, -689942508603024688L) + test(24530734973246035L, -4976536915346383672L, -90929133590073966L) + test(915668791878818L, -4915702564252847L, -3436153355352311231L) + test(-59487608720960501L, 2234272329433906652L, -491145452224512365L) + test(-935777346233643464L, 2234022931260640741L, -7726888105936443458L) + test(-539196324963981948L, 1233384294780865907L, -8064328899098291942L) + test(-302740552339519239L, 1652272762436229815L, -3379936785683182277L) + test(-1602328337662720444L, -5891195966699023422L, 5017273391344774367L) + test(1971437877011804292L, 6123334000940359947L, 5939021122948580484L) + test(3518273874050862283L, -7935043146462869940L, -8178997459486413381L) + test(989386049294028022L, 3631504400505165814L, 5025727419987895939L) + test(1075600553777136761L, 8162668046881939535L, 2430740540606242760L) + test(555876997051543592L, -1422006546765159905L, -7211022146415941068L) + test(1442987791832810570L, 3172003226122803882L, 8391676993961733131L) + test(122174343239443206L, 592078109511582332L, 3806455273225175653L) + test(-555975358284841098L, -2610695041141095892L, 3928430928909536969L) + test(1217820260754824228L, -2566343358431797989L, -8753629401971345682L) + test(-843540703271762806L, 2010390971620435041L, -7740076278033066915L) + test(28227414827282063L, 1691814723551530731L, 307778322255183098L) + test(-3487482743675782331L, 8885183126228404590L, -7240447464066348779L) + test(-641218088086423374L, -5793475349478143447L, 2041673650588512538L) + test(491218135799199820L, -3483174304311045377L, -2601470510458659970L) + test(-61083956648009538L, -331097881159246733L, 3403223576515274855L) + test(-1760654512150512675L, -6642702867806073297L, 4889326503714183951L) + } + +} diff --git a/test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK15.scala b/test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK17.scala similarity index 98% rename from test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK15.scala rename to test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK17.scala index 5bda94aa7a..181f15cccf 100644 --- a/test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK15.scala +++ b/test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK17.scala @@ -21,7 +21,7 @@ import org.junit.Assume._ import org.scalajs.testsuite.utils.AssertThrows.assertThrows import org.scalajs.testsuite.utils.Platform -class InputStreamTestOnJDK15 { +class InputStreamTestOnJDK17 { /** InputStream that only ever skips max bytes at once */ def lowSkipStream(max: Int, seq: Seq[Int]): InputStream = new SeqInputStreamForTest(seq) { require(max > 0) diff --git a/test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/lang/ConstableTest.scala b/test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/lang/ConstableTest.scala similarity index 100% rename from test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/lang/ConstableTest.scala rename to test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/lang/ConstableTest.scala diff --git a/test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/lang/ConstantDescTest.scala b/test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/lang/ConstantDescTest.scala similarity index 100% rename from test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/lang/ConstantDescTest.scala rename to test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/lang/ConstantDescTest.scala diff --git a/test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/lang/StringTestOnJDK15.scala b/test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/lang/StringTestOnJDK17.scala similarity index 99% rename from test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/lang/StringTestOnJDK15.scala rename to test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/lang/StringTestOnJDK17.scala index 25ed53f386..21ffe9cf1d 100644 --- a/test-suite/shared/src/test/require-jdk15/org/scalajs/testsuite/javalib/lang/StringTestOnJDK15.scala +++ b/test-suite/shared/src/test/require-jdk17/org/scalajs/testsuite/javalib/lang/StringTestOnJDK17.scala @@ -17,7 +17,7 @@ import org.junit.Assert._ import org.scalajs.testsuite.utils.AssertThrows.assertThrows -class StringTestOnJDK15 { +class StringTestOnJDK17 { // indent and transform are available since JDK 12 but we're not testing them separately diff --git a/test-suite/shared/src/test/require-jdk21/org/scalajs/testsuite/javalib/lang/MathTestOnJDK21.scala b/test-suite/shared/src/test/require-jdk21/org/scalajs/testsuite/javalib/lang/MathTestOnJDK21.scala new file mode 100644 index 0000000000..b57216847d --- /dev/null +++ b/test-suite/shared/src/test/require-jdk21/org/scalajs/testsuite/javalib/lang/MathTestOnJDK21.scala @@ -0,0 +1,129 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.javalib.lang + +import java.math.BigInteger +import java.util.SplittableRandom + +import org.junit.Test +import org.junit.Assert._ + +class MathTestOnJDK21 { + + @Test def testUnsignedMultiplyHigh(): Unit = { + def test(expected: Long, x: Long, y: Long): Unit = + assertEquals(expected, Math.unsignedMultiplyHigh(x, y)) + + test(-4655528149793241951L, -3491544249150011246L, -1435735621922138183L) + test(4723475310515791226L, -5748086171833985033L, 6861570794713764439L) + test(1844940925490449716L, 2113050876768271470L, -2340575756699630680L) + test(702124944283937448L, 2364425117167296916L, 5477830133394302018L) + test(8604154842195983318L, -765591305295706482L, 8976713476968422536L) + test(721433542721114290L, 2866411935332571322L, 4642772995997153672L) + test(198977852337409286L, 901957528446724377L, 4069474895038101365L) + test(763163907759431674L, 5160783458443768226L, 2727858939653224648L) + test(3387911794594526182L, 4369265381816556396L, -4143208729009548760L) + test(7271333002323704409L, -1757891743212949508L, 8037246439257825352L) + test(2439931237179080842L, -6474169961931828112L, 3759324157819640760L) + test(6061120068222317095L, -2837073725411217492L, 7162734907512678039L) + test(34351029958289581L, 2814331433271609755L, 225156373132738607L) + test(5537345867541993285L, -1010313517008906026L, 5858194527486398591L) + test(-5335982525234939944L, -2717969804981747482L, -3070411578621565733L) + test(8785453866123381013L, -8027031729027070236L, -2893241858030230601L) + test(846995399721837047L, 8016550578873635720L, 1949006273527852101L) + test(4706098605093773886L, -8117794498444021304L, 8404745896106734176L) + test(8161884790484487335L, -1014727160850354519L, 8636992531719324619L) + test(-8513922914337796266L, -7911312134726022659L, -1055125873522512866L) + test(5454317624646219097L, -5009725974720812208L, 7487851886286018345L) + test(2492015203153257923L, 5501297108904060131L, 8356132339400095318L) + test(251150029847529372L, 2825249782885673228L, 1639819725946463245L) + test(701745563060384928L, -862030064654873400L, 736146223360275347L) + test(-7730043035170177278L, -81403377518056314L, -7682541838496528017L) + test(7236963176581899295L, -5477193291802744595L, -8153526534093331950L) + test(626928141670112235L, 1514687386182852255L, 7635095589684134756L) + test(413586791425617421L, 2206621030247415833L, 3457471667819472989L) + test(5275688490004831530L, 5303665000787184389L, -97305454699880484L) + test(-4547690235532216689L, -4468474298913285824L, -104539126294685679L) + test(366277061497431976L, 2447428253923437996L, 2760701647814215009L) + test(6869979725009155017L, -6471585201942397935L, -7864107205517734491L) + test(3791302630224081431L, -3133537597544878918L, 4567115935815546723L) + test(1740958585965607218L, -1557363676960182057L, 1901491749479209603L) + test(1523197267704952893L, 6322993002648583029L, 4443786377646975729L) + test(-5259964406327079828L, -1435127053681489364L, -4147506711722433774L) + test(461442857371067152L, -3905080403415491898L, 585360691015982209L) + test(-5941004277830287560L, -823789104926622437L, -5356420579401640852L) + test(-8706994437197957328L, -7948824177902058651L, -1332242280026816373L) + test(-5824918531825640540L, -1108632841430213984L, -5017854248578702419L) + test(-8207168723011034838L, -6116459797997975723L, -3127808865551126328L) + test(73145162458483087L, 984457260399802488L, 1370592860022769137L) + test(1646786118016093153L, 1899627239649590099L, -2455268783649962915L) + test(3040972539165017704L, -1806601545829941920L, 3371127505138255794L) + test(-7035139036718491109L, -5137998055321179172L, -2629554588180521105L) + test(3814121656202007379L, 6827719277638332778L, -8141966856464601543L) + test(846533671710128614L, 928822269112330348L, -1634281118080467412L) + test(2465515036079121285L, -6936668916098982255L, 3951383831786888133L) + test(-8705991089541820518L, -4627352440152123018L, -5444349891042507930L) + test(-1725081446201736077L, -102123348177150214L, -1631993003537285149L) + test(3162229868737586796L, -3928062681119615377L, 4017778440996329527L) + test(-5644865433298065278L, -1848848427461824206L, -4218857359905179258L) + test(8602545182711575119L, -7248610265558893317L, -4275726644671484627L) + test(574312046131959731L, 3094727123103690882L, 3423302576293014745L) + test(1498569573726223576L, -7522287153787738568L, 2530444268837256898L) + test(9201094795447111684L, -7474596509362715910L, -2977553571653290816L) + test(3390173448745695247L, 4278016163532080543L, -3828364997071413384L) + test(1203823557488246474L, 7917135674481131658L, 2804881208044173681L) + test(78424886362034171L, 980323435081771015L, 1475720926338487149L) + test(2441060259411459766L, 5798689165809470750L, 7765481574589679868L) + test(1218713916031010010L, 1847981405952407601L, -6281414035388066866L) + test(3425204597140630L, 23488433971625858L, 2689999370748729443L) + test(1973267136565988242L, -1929721405614247783L, 2203808433804858703L) + test(68657367104675341L, 1589652507076814732L, 796718071475649415L) + test(3001541031524236691L, 8704517430100436852L, 6360910835079676298L) + test(681950840803061472L, 4077993211593106210L, 3084794892591474962L) + test(-8032203953933386693L, -1297027813828962384L, -7244555458827535130L) + test(1705455955228875706L, 7677829613973803154L, 4097526399626386343L) + test(-7072562212974300041L, -4950443113989053629L, -2900512372222814349L) + test(1553202529265923923L, -5818129186601545240L, 2268778469225152008L) + test(468774576517241981L, 4408923425379013709L, 1961332463044936276L) + test(3713249757582606154L, 3805244009484784339L, -445962050491898861L) + test(-6492270872661604844L, -3676904101986751446L, -3516243262736404968L) + test(-1570352970576497130L, -498180996573724276L, -1101931219921881504L) + test(5938952698782807216L, -2212548245883761340L, 6748368792775950262L) + test(6965609214550264138L, -44143931286210318L, 6982318232414802091L) + test(5427827011873507182L, -6210411243958046988L, 8182658739140523648L) + test(-4010444622072654726L, -2893048189377704445L, -1325236533881556505L) + test(-751397756032611136L, -297794630894262553L, -461046011882216476L) + test(547299415238293930L, 4622152711521934473L, 2184240304182297561L) + test(378891801518625290L, -4298572905176069751L, 494008731657528393L) + test(1743296278846509964L, 2577934180605703203L, -5972360304541326595L) + test(1039517173945592548L, 5277562622670643081L, 3633440025065309218L) + test(2800417145934889950L, 4637163034857372585L, -7306618526608114613L) + test(1678445276921448048L, 4821090766475680470L, 6422167091396679498L) + test(6227359204013573541L, -4999273910373625291L, 8542461897755272268L) + test(-5499015746244333303L, -1511897164914852634L, -4343077705835106861L) + test(2697793722365988629L, 6211672456183761935L, 8011612123978580051L) + test(1911468969140043410L, -5555150943474614346L, 2735145185110353700L) + test(2743615826392469850L, 6136958276954783995L, 8246883342207528968L) + test(1464762398133852646L, -6115223270905044522L, 2191140697019557187L) + test(457204414584490485L, -5068587596916665104L, 630425637481565869L) + test(-6229688730810143122L, -4425525343688714952L, -2373612516159392266L) + test(388393998755070731L, 797622384306174158L, 8982451891732844522L) + test(5110014275194731110L, 8574583813914626477L, -7453426194589668982L) + test(6332629791985833635L, -6221980493997883829L, -8891025443683788065L) + test(1100817927132096284L, -5992086725542985450L, 1630434784827422367L) + test(-6893218526017086868L, -566288691658266700L, -6527308893032530287L) + test(6838523840226613906L, 7591397219780968602L, -1829447485155925067L) + test(2870893831454164280L, 3649310365003545712L, -3934784696536939433L) + } + +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/TryFinallyTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/TryFinallyTest.scala new file mode 100644 index 0000000000..57d6119aa4 --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/TryFinallyTest.scala @@ -0,0 +1,272 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.compiler + +import scala.collection.mutable + +import org.junit.Test +import org.junit.Assert._ + +// Much of the point of this test class is to test `return`s inside `try..finally`s +// scalastyle:off return + +class TryFinallyTest { + + /* Some of these tests are ported from the partest run/finally.scala. + * We have copies of them in our own test suite to more quickly identify + * any issues with our compilation scheme for try..finally. On JS it is + * straightforward, but it is a huge beast in Wasm. + */ + + type SideEffect = Any => Unit + + @noinline + def test(body: SideEffect => Unit)(expectedSideEffects: String*): Unit = { + val sideEffects = mutable.ListBuffer.empty[String] + + try { + body(x => sideEffects += ("" + x)) + } catch { + case e: Throwable => + sideEffects += ("CAUGHT: " + e) + } + + if (!sideEffects.sameElements(expectedSideEffects)) { + // Custom message for easier debugging + fail( + "Expected side effects:" + + expectedSideEffects.mkString("\n* ", "\n* ", "\n") + + "but got:" + + sideEffects.mkString("\n* ", "\n* ", "\n")) + } + } + + // test that finally is not covered by any exception handlers. + @Test + def throwCatchFinally(): Unit = { + test { println => + def bar(): Unit = { + try { + println("hi") + } catch { + case e: Throwable => println("SHOULD NOT GET HERE") + } finally { + println("In Finally") + throw new RuntimeException("ouch") + } + } + + try { + bar() + } catch { + case e: Throwable => println(e) + } + } ( + "hi", + "In Finally", + "java.lang.RuntimeException: ouch" + ) + } + + // test that finally is not covered by any exception handlers. + // return in catch (finally is executed) + @Test + def retCatch(): Unit = { + test { println => + def retCatchInner(): Unit = { + try { + throw new Exception + } catch { + case e: Throwable => + println(e) + return + } finally { + println("in finally") + } + } + + retCatchInner() + } ( + "java.lang.Exception", + "in finally" + ) + } + + // throw in catch (finally is executed, exception propagated) + @Test + def throwCatch(): Unit = { + test { println => + try { + throw new Exception + } catch { + case e: Throwable => + println(e) + throw e + } finally { + println("in finally") + } + } ( + "java.lang.Exception", + "in finally", + "CAUGHT: java.lang.Exception" + ) + } + + // return inside body (finally is executed) + @Test + def retBody(): Unit = { + test { println => + def retBodyInner(): Unit = { + try { + return + } catch { + case e: Throwable => + println(e) + throw e + } finally println("in finally") + } + + retBodyInner() + } ( + "in finally" + ) + } + + // throw inside body (finally and catch are executed) + @Test + def throwBody(): Unit = { + test { println => + try { + throw new Exception + } catch { + case e: Throwable => + println(e) + } finally { + println("in finally") + } + } ( + "java.lang.Exception", + "in finally" + ) + } + + // return inside finally (each finally is executed once) + @Test + def retFinally(): Unit = { + test { println => + def retFinallyInner(): Unit = { + try { + try { + println("body") + } finally { + println("in finally 1") + return + } + } finally { + println("in finally 2") + } + } + + retFinallyInner() + } ( + "body", + "in finally 1", + "in finally 2" + ) + } + + // throw inside finally (finally is executed once, exception is propagated) + @Test + def throwFinally(): Unit = { + test { println => + try { + try { + println("body") + } finally { + println("in finally") + throw new Exception + } + } catch { + case e: Throwable => println(e) + } + } ( + "body", + "in finally", + "java.lang.Exception" + ) + } + + // nested finally blocks with return value + @Test + def nestedFinallyBlocks(): Unit = { + test { println => + def nestedFinallyBlocksInner(): Int = { + try { + try { + return 10 + } finally { + try { () } catch { case _: Throwable => () } + println("in finally 1") + } + } finally { + println("in finally 2") + } + } + + assertEquals(10, nestedFinallyBlocksInner()) + } ( + "in finally 1", + "in finally 2" + ) + } + + @Test + def nonDefaultableTryResultType_Issue5165(): Unit = { + test { println => + // after the optimizer, some has type Some! (a non-nullable reference type) + val some = try { + println("in try") + Some(1) + } finally { + println("in finally") + } + assertEquals(1, some.value) + } ( + "in try", + "in finally" + ) + } + + @Test + def nonDefaultableLabeledResultType_Issue5165(): Unit = { + test { println => + /* After the optimizer, the result type of the Labeled block that gets + * inlined is a Some! (a non-nullable reference type). + */ + @inline def nonDefaultableLabeledResultTypeInner(): Some[Int] = { + try { + println("in try") + return Some(1) + } finally { + println("in finally") + } + } + + val some = nonDefaultableLabeledResultTypeInner() + assertEquals(1, some.value) + } ( + "in try", + "in finally" + ) + } +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/LongTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/LongTest.scala index 8566a378a2..df572ef989 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/LongTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/LongTest.scala @@ -27,6 +27,8 @@ class LongTest { final val MinRadix = Character.MIN_RADIX final val MaxRadix = Character.MAX_RADIX + @noinline def hideFromOptimizer(x: Long): Long = x + @Test def reverseBytes(): Unit = { assertEquals(0x14ff01d49c68abf5L, JLong.reverseBytes(0xf5ab689cd401ff14L)) assertEquals(0x780176af73b18fc7L, JLong.reverseBytes(0xc78fb173af760178L)) @@ -272,6 +274,33 @@ class LongTest { assertEquals("89000000005", JLong.toString(89000000005L)) assertEquals("-9223372036854775808", JLong.toString(JLong.MIN_VALUE)) assertEquals("9223372036854775807", JLong.toString(JLong.MAX_VALUE)) + + // Corner cases of the approximation inside RuntimeLong.toUnsignedString + + // Approximated quotient is too high + assertEquals("2777572447999999934", JLong.toString(0x268beb6cdcf3bfbeL)) + assertEquals("3611603422999999979", JLong.toString(0x321efe2d997ff5ebL)) + assertEquals("7742984029999999701", JLong.toString(0x6b749af381ac2ad5L)) + assertEquals("2161767614999999954", JLong.toString(0x1e0024313b04b5d2L)) + assertEquals("5388513109999999953", JLong.toString(0x4ac7d81fbd15dbd1L)) + assertEquals("3713052774999999769", JLong.toString(0x338769d386274519L)) + assertEquals("-5647508785999999800", JLong.toString(0xb1a004ae50928cc8L)) + assertEquals("-1406561754999999938", JLong.toString(0xec7ae3893e93323eL)) + assertEquals("-8621287367999999564", JLong.toString(0x885b08d0fbcc31b4L)) + assertEquals("-8876380314999999920", JLong.toString(0x84d0c321f127b250L)) + assertEquals("-5002322935999999598", JLong.toString(0xba942dcb0bee5192L)) + assertEquals("-4971399139999999950", JLong.toString(0xbb020ad25f9e1832L)) + assertEquals("-8515854999999999733", JLong.toString(0x89d19aff1644110bL)) + assertEquals("-4806014223999999712", JLong.toString(0xbd4d9b86d1016120L)) + assertEquals("-9133328502999999878", JLong.toString(0x813fe61df1bc1a7aL)) + assertEquals("-7816299703999999849", JLong.toString(0x9386ecd4ed16d097L)) + assertEquals("-7259227631999999909", JLong.toString(0x9b420aee02f0a05bL)) + assertEquals("-2526704305999999860", JLong.toString(0xdcef57d21c6b8c8cL)) + assertEquals("-1100666257999999982", JLong.toString(0xf0b9a5deb3a6cc12L)) + + // Approximated quotient is too low + assertEquals("7346875325000000000", JLong.toString(0x65f5582ec3b52200L)) + assertEquals("-7993685585000000000", JLong.toString(0x9110b95013ea1600L)) } @Test def toStringRadix(): Unit = { @@ -659,8 +688,12 @@ class LongTest { } @Test def divideUnsigned(): Unit = { - def test(dividend: Long, divisor: Long, result: Long): Unit = - assertEquals(result, JLong.divideUnsigned(dividend, divisor)) + @inline def test(x: Long, y: Long, result: Long): Unit = { + assertEquals(result, JLong.divideUnsigned(x, y)) + assertEquals(result, JLong.divideUnsigned(hideFromOptimizer(x), y)) + assertEquals(result, JLong.divideUnsigned(x, hideFromOptimizer(y))) + assertEquals(result, JLong.divideUnsigned(hideFromOptimizer(x), hideFromOptimizer(y))) + } test(-9223372034182170740L, 53886L, 171164533265177L) test(-9223372036854775807L, 1L, -9223372036854775807L) @@ -721,8 +754,12 @@ class LongTest { } @Test def remainderUnsigned(): Unit = { - def test(dividend: Long, divisor: Long, result: Long): Unit = - assertEquals(result, JLong.remainderUnsigned(dividend, divisor)) + @inline def test(x: Long, y: Long, result: Long): Unit = { + assertEquals(result, JLong.remainderUnsigned(x, y)) + assertEquals(result, JLong.remainderUnsigned(hideFromOptimizer(x), y)) + assertEquals(result, JLong.remainderUnsigned(x, hideFromOptimizer(y))) + assertEquals(result, JLong.remainderUnsigned(hideFromOptimizer(x), hideFromOptimizer(y))) + } test(97062081516L, 772L, 668L) test(-9223372036854775472L, 49L, 43L) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ArrayListTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ArrayListTest.scala index 9b9812f93c..400da32882 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ArrayListTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ArrayListTest.scala @@ -13,6 +13,11 @@ package org.scalajs.testsuite.javalib.util import org.junit.Test +import org.junit.Assert._ +import org.junit.Assume._ + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows +import org.scalajs.testsuite.utils.Platform import java.{util => ju} @@ -20,7 +25,7 @@ import scala.reflect.ClassTag class ArrayListTest extends AbstractListTest { - override def factory: AbstractListFactory = new ArrayListFactory + override def factory: ArrayListFactory = new ArrayListFactory @Test def ensureCapacity(): Unit = { // note that these methods become no ops in js @@ -29,6 +34,86 @@ class ArrayListTest extends AbstractListTest { al.ensureCapacity(34) al.trimToSize() } + + @Test def constructorInitialCapacity(): Unit = { + val al1 = new ju.ArrayList(0) + assertTrue(al1.size() == 0) + assertTrue(al1.isEmpty()) + + val al2 = new ju.ArrayList(2) + assertTrue(al2.size() == 0) + assertTrue(al2.isEmpty()) + + assertThrows(classOf[IllegalArgumentException], new ju.ArrayList(-1)) + } + + @Test def constructorNullThrowsNullPointerException(): Unit = { + assumeTrue("assumed compliant NPEs", Platform.hasCompliantNullPointers) + assertThrows(classOf[NullPointerException], new ju.ArrayList(null)) + } + + @Test def testClone(): Unit = { + val al1 = factory.fromElements[Int](1, 2) + val al2 = al1.clone().asInstanceOf[ju.ArrayList[Int]] + al1.add(100) + al2.add(200) + assertTrue(Array[Int](1, 2, 100).sameElements(al1.toArray())) + assertTrue(Array[Int](1, 2, 200).sameElements(al2.toArray())) + } + + @Test def removeRangeFromIdenticalIndices(): Unit = { + val al = new ArrayListRangeRemovable[Int]( + TrivialImmutableCollection(-175, 24, 7, 44)) + val expected = Array[Int](-175, 24, 7, 44) + al.removeRangeList(0, 0) + assertTrue(al.toArray().sameElements(expected)) + al.removeRangeList(1, 1) + assertTrue(al.toArray().sameElements(expected)) + al.removeRangeList(al.size, al.size) // no op + assertTrue(al.toArray().sameElements(expected)) + } + + @Test def removeRangeFromToInvalidIndices(): Unit = { + val al = new ArrayListRangeRemovable[Int]( + TrivialImmutableCollection(175, -24, -7, -44)) + + assertThrows( + classOf[java.lang.IndexOutOfBoundsException], + al.removeRangeList(-1, 2) + ) // fromIndex < 0 + assertThrows( + classOf[java.lang.IndexOutOfBoundsException], + al.removeRangeList(0, al.size + 1) + ) // toIndex > size + assertThrows( + classOf[java.lang.IndexOutOfBoundsException], + al.removeRangeList(2, -1) + ) // toIndex < fromIndex + } + + @Test def removeRangeFromToFirstTwoElements(): Unit = { + val al = new ArrayListRangeRemovable[Int]( + TrivialImmutableCollection(284, -27, 995, 500, 267, 904)) + val expected = Array[Int](995, 500, 267, 904) + al.removeRangeList(0, 2) + assertTrue(al.toArray().sameElements(expected)) + } + + @Test def removeRangeFromToTwoElementsFromMiddle(): Unit = { + val al = new ArrayListRangeRemovable[Int]( + TrivialImmutableCollection(7, 9, -1, 20)) + val expected = Array[Int](7, 20) + al.removeRangeList(1, 3) + assertTrue(al.toArray().sameElements(expected)) + } + + @Test def removeRangeFromToLastTwoElementsAtTail(): Unit = { + val al = new ArrayListRangeRemovable[Int]( + TrivialImmutableCollection(50, 72, 650, 12, 7, 28, 3)) + val expected = Array[Int](50, 72, 650, 12, 7) + al.removeRangeList(al.size - 2, al.size) + assertTrue(al.toArray().sameElements(expected)) + } } class ArrayListFactory extends AbstractListFactory { @@ -37,4 +122,13 @@ class ArrayListFactory extends AbstractListFactory { override def empty[E: ClassTag]: ju.ArrayList[E] = new ju.ArrayList[E] + + override def fromElements[E: ClassTag](coll: E*): ju.ArrayList[E] = + new ju.ArrayList[E](TrivialImmutableCollection(coll: _*)) +} + +class ArrayListRangeRemovable[E](c: ju.Collection[_ <: E]) extends ju.ArrayList[E](c) { + def removeRangeList(fromIndex: Int, toIndex: Int): Unit = { + removeRange(fromIndex, toIndex) + } } diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/CollectionTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/CollectionTest.scala index 787d88a4c3..c73e6acccd 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/CollectionTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/CollectionTest.scala @@ -117,6 +117,16 @@ trait CollectionTest extends IterableTest { assertFalse(coll.contains(TestObj(200))) } + @Test def isEmpty(): Unit = { + val coll = factory.empty[Int] + assertTrue(coll.size() == 0) + assertTrue(coll.isEmpty()) + + val nonEmpty = factory.fromElements[Int](1) + assertTrue(nonEmpty.size() == 1) + assertFalse(nonEmpty.isEmpty()) + } + @Test def removeString(): Unit = { val coll = factory.empty[String] diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ListTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ListTest.scala index 8835696b00..98773fef7a 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ListTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/ListTest.scala @@ -96,6 +96,19 @@ trait ListTest extends CollectionTest with CollectionsTestBase { assertThrows(classOf[IndexOutOfBoundsException], lst.get(lst.size)) } + @Test def addAllIndexBounds(): Unit = { + val al = factory.fromElements[String]("one", "two", "three") + + val coll = factory.fromElements[String]("foo") + assertThrows(classOf[IndexOutOfBoundsException], al.addAll(-1, coll)) + assertThrows(classOf[IndexOutOfBoundsException], al.addAll(al.size + 1, coll)) + + assertThrows(classOf[IndexOutOfBoundsException], + al.addAll(-1, TrivialImmutableCollection("foo"))) + assertThrows(classOf[IndexOutOfBoundsException], + al.addAll(al.size + 1, TrivialImmutableCollection("foo"))) + } + @Test def removeStringRemoveIndex(): Unit = { val lst = factory.empty[String]