From b53a090206eb72be2c94893394297503106d0f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 21 Apr 2025 11:41:44 +0200 Subject: [PATCH 01/36] Towards 1.19.1. --- .../org/scalajs/ir/ScalaJSVersions.scala | 2 +- project/BinaryIncompatibilities.scala | 48 ------------------- project/Build.scala | 2 +- 3 files changed, 2 insertions(+), 50 deletions(-) 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..4de34d7f0b 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.19.0", + current = "1.19.1-SNAPSHOT", binaryEmitted = "1.19" ) diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 5435860a02..4713fe6bf8 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -5,56 +5,12 @@ 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"), ) val LinkerInterface = Seq( - // private, not an issue - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.interface.Semantics.this"), ) val SbtPlugin = Seq( @@ -64,10 +20,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..80bfe9792c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -396,7 +396,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_", "") From 1e6540a60cc3f1bf7737c3ff1318ae10e8b9b026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 28 Apr 2025 20:04:28 +0200 Subject: [PATCH 02/36] Fix #5159: Register static module dependency on used lambda classes. When we use a lambda class, we implicitly instantiate it. That constitutes a static dependency, which we previously failed to register. --- Jenkinsfile | 5 +++++ .../main/scala/org/scalajs/linker/analyzer/Analyzer.scala | 1 + 2 files changed, 6 insertions(+) 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/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..2379ca00c6 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 @@ -1488,6 +1488,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) } } } From fd90409ff6bee7702b9a23bc540df4ef1d875d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 4 May 2025 17:46:18 +0200 Subject: [PATCH 03/36] Reorganize LinkTimeProperties. * Move it to `frontend`. * Make it public. * Move the logic of `validate` and `transformLinkTimeProperty` to their respective call sites. * Construct them from a CoreSpec, rather than being contained by CoreSpec. These changes better isolate the data (`LinkTimeProperties`) from the transformations we apply to that data (the logic in `Analyzer` and `Desugarer`). --- .../scalajs/linker/analyzer/Analyzer.scala | 13 +++-- .../scalajs/linker/frontend/Desugarer.scala | 19 +++++-- .../LinkTimeProperties.scala | 52 ++++++++----------- .../scalajs/linker/standard/CoreSpec.scala | 3 -- project/BinaryIncompatibilities.scala | 3 ++ 5 files changed, 50 insertions(+), 40 deletions(-) rename linker/shared/src/main/scala/org/scalajs/linker/{standard => frontend}/LinkTimeProperties.scala (50%) 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 2379ca00c6..22d3752fd4 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,6 +47,8 @@ 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) @@ -55,7 +57,7 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean, 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 @@ -1539,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/frontend/Desugarer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Desugarer.scala index 44e2f66d09..57f8eeb366 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") { @@ -118,7 +120,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 +137,17 @@ 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 NewLambda(descriptor, fun) => implicit val pos = tree.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/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/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 4713fe6bf8..2e94162e72 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -8,6 +8,9 @@ object BinaryIncompatibilities { ) val Linker = Seq( + // 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( From 4913fc9661fc39681221412a94577f57d30ebe1d Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Mon, 5 May 2025 15:51:05 +0900 Subject: [PATCH 04/36] Check index on bounds in ArrayList addAll and removeRange. removeRange should throws an `IndexOutOfBoundsException` if fromIndex or toIndex is out of range. addAll should also throw when the index is not within bounds. --- .../src/main/scala/java/util/ArrayList.scala | 7 +- .../javalib/util/ArrayListTest.scala | 66 +++++++++++++++++++ .../testsuite/javalib/util/ListTest.scala | 13 ++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/javalib/src/main/scala/java/util/ArrayList.scala b/javalib/src/main/scala/java/util/ArrayList.scala index 68b9705f62..62de296cab 100644 --- a/javalib/src/main/scala/java/util/ArrayList.scala +++ b/javalib/src/main/scala/java/util/ArrayList.scala @@ -81,13 +81,16 @@ class ArrayList[E] private (private[ArrayList] val inner: js.Array[E]) override def addAll(index: Int, c: Collection[_ <: E]): Boolean = { c match { case other: ArrayList[_] => + checkIndexOnBounds(index) inner.splice(index, 0, other.inner.toSeq: _*) other.size() > 0 case _ => super.addAll(index, c) } } - override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = + override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = { + if (fromIndex < 0 || toIndex > size() || toIndex < fromIndex) + throw new IndexOutOfBoundsException() inner.splice(fromIndex, toIndex - fromIndex) - + } } 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..25d9bbb7de 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,9 @@ package org.scalajs.testsuite.javalib.util import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows import java.{util => ju} @@ -29,6 +32,60 @@ class ArrayListTest extends AbstractListTest { al.ensureCapacity(34) al.trimToSize() } + + @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 +94,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/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] From a088ec4b5f3e22f1f287365ef84bf6ec9ced3adb Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Fri, 2 May 2025 10:03:33 +0900 Subject: [PATCH 05/36] Wasm: Implement ju.ArrayList without js.Array. The original implementation of ju.ArrayList uses js.Array as its internal data structure. When compiling to Wasm, operations on ju.ArrayList require JS interop calls to access the underlying js.Array, which causes a slow performance. This commit introduces an implementation of ju.ArrayList for the Wasm backend. This version uses Scala's Array instead of js.Array for better performance. --- .../src/main/scala/java/util/ArrayList.scala | 140 +++++++++++++++--- .../javalib/util/ArrayListTest.scala | 30 +++- .../javalib/util/CollectionTest.scala | 10 ++ 3 files changed, 158 insertions(+), 22 deletions(-) diff --git a/javalib/src/main/scala/java/util/ArrayList.scala b/javalib/src/main/scala/java/util/ArrayList.scala index 62de296cab..1c67de682b 100644 --- a/javalib/src/main/scala/java/util/ArrayList.scala +++ b/javalib/src/main/scala/java/util/ArrayList.scala @@ -14,75 +14,154 @@ 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[_] => checkIndexOnBounds(index) - inner.splice(index, 0, other.inner.toSeq: _*) + 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) } @@ -91,6 +170,25 @@ class ArrayList[E] private (private[ArrayList] val inner: js.Array[E]) override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = { if (fromIndex < 0 || toIndex > size() || toIndex < fromIndex) throw new IndexOutOfBoundsException() - inner.splice(fromIndex, toIndex - fromIndex) + 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/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 25d9bbb7de..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 @@ -14,8 +14,10 @@ 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} @@ -23,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 @@ -33,6 +35,32 @@ class ArrayListTest extends AbstractListTest { 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)) 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] From c399a080a86503a67dd235a02561feb8b8d96c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 22 Apr 2025 10:02:38 +0200 Subject: [PATCH 06/36] Bump the version to 1.20.0-SNAPSHOT for the upcoming changes. As well as the IR version to 1.20-SNAPSHOT. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4de34d7f0b..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.1-SNAPSHOT", - binaryEmitted = "1.19" + current = "1.20.0-SNAPSHOT", + binaryEmitted = "1.20-SNAPSHOT" ) /** Helper class to allow for testing of logic. */ From 5e842d868dd16c0f15bfbb4c583e6f8eac24c87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 3 Jan 2025 17:22:15 +0100 Subject: [PATCH 07/36] Fix #4997: Add `linkTimeIf` for link-time conditional branching. Thanks to our optimizer's ability to inline, constant-fold, and then eliminate dead code, we have been able to write link-time conditional branches for a long time. Typical examples include polyfills, as illustrated in the documentation of `LinkingInfo`: if (esVersion >= ESVersion.ES2018 || featureTest()) useES2018Feature() else usePolyfill() which gets folded away to nothing but useES2018Feature() when linking for ES2018+. However, this only works because both branches can *link* during the initial reachability analysis. We cannot use the same technique when one of the branches would refuse to link in the first place. The canonical example is the usage of the JS `**` operator, which does not link below ES2016. The following snippet produces good code when linking for ES2016+, but does not link at all for ES2015: def pow(x: Double, y: Double): Double = { if (esVersion >= ESVersion.ES2016) { (x.asInstanceOf[js.Dynamic] ** y.asInstanceOf[js.Dynamic]) .asInstanceOf[Double] } { Math.pow(x, y) } } --- This commit introduces `LinkingInfo.linkTimeIf`, a conditional branch that is guaranteed by spec to be resolved at link-time. Using a `linkTimeIf` instead of the `if` in `def pow`, we can successfully link the fallback branch on ES2015, because the then branch is not even followed by the reachability analysis. In order to provide that guarantee, the corresponding `LinkTimeIf` IR node has strong requirements on its condition. It must be a "link-time expression", which is guaranteed to be resolved at link-time. A link-time expression tree must be of the form: * A `Literal` (of type `int`, `boolean` or `string`, although `string`s are not actually usable here). * A `LinkTimeProperty`. * One of the boolean operators. * One of the int comparison operators. * A nested `LinkTimeIf` (used to encode short-circuiting boolean `&&` and `||`). The `ClassDefChecker` validates the above property, and ensures that link-time expression trees are *well-typed*. Normally that is the job of the IR checker. Here we *can* do in `ClassDefChecker` because we only have the 3 primitive types to deal with; and we *must* do it then, because the reachability analysis itself is only sound if all link-time expression trees are well-typed. The reachability analysis algorithm itself is not affected by `LinkTimeIf`. Instead, we resolve link-time branches when building the `Infos` of methods. We follow only the branch that is taken. This means that `Infos` builders now require access to the `linkTimeProperties` derived from the `coreSpec`, but that is the only additional piece of complexity in that area. `LinkTimeIf`s nodes are later removed from the trees during desugaring. --- At the language and compiler level, we introduce `LinkingInfo.linkTimeIf` as a primitive for `LinkTimeIf`. We need a dedicated method to compile link-time expression trees, which does incur some duplication, unfortunately. Other than that, `linkTimeIf` is straightforward, by itself. The problem is that the whole point of `linkTimeIf` is that we can refer to *link-time properties*, and not just literals. However, our link-time properties are all hidden behind regular method calls, such as `LinkInfo.esVersion`. For optimizer-based branching with `if`s, that is fine, as the method is always inlined, and the optimizer can then see the constant. However, for `linkTimeIf`, that does not work, as it does not follow the requirements of a link-time expression tree. If we were on Scala 3 only, we could declare `esVersion` and its friends as an `inline def`, as follows: inline def esVersion: Int = linkTimePropertyInt("core/esVersion") The `inline` keyword is guaranteed by the language to be resolved at *compile*-time. Since the `linkTimePropertyInt` method is itself a primitive replaced by a `LinkTimeProperty`, by the time we reach our backend, we would see the latter, and all would be well. The same cannot be said for the `@inline` optimizer hint, which is all we have. We therefore add another language-level feature: `@linkTimeProperty`. This annotation can (currently) only be used in our own library. By contract, it must only be used on a method whose body is the corresponding `linkTimePropertyX` primitive. With it, we can define `esVersion` as: @inline @linkTimeProperty("core/esVersion") def esVersion: Int = linkTimePropertyInt("core/esVersion") That annotation makes the body public, in a way. That means the compiler back-end can now replace *call sites* to `esVersion` by the `LinkTimeProperty`. Semantically, `@linkTimeProperty` does nothing more than guaranteed inlining (with strong restrictions on the shape of body). Co-authored-by: Rikito Taniguchi --- .../org/scalajs/nscplugin/GenJSCode.scala | 87 ++++++++++ .../org/scalajs/nscplugin/JSDefinitions.scala | 3 + .../org/scalajs/nscplugin/JSPrimitives.scala | 4 +- .../nscplugin/test/LinkTimeIfTest.scala | 109 +++++++++++++ .../main/scala/org/scalajs/ir/Hashers.scala | 7 + .../main/scala/org/scalajs/ir/Printers.scala | 9 ++ .../scala/org/scalajs/ir/Serializers.scala | 16 +- .../src/main/scala/org/scalajs/ir/Tags.scala | 3 + .../scala/org/scalajs/ir/Transformers.scala | 3 + .../scala/org/scalajs/ir/Traversers.scala | 5 + .../src/main/scala/org/scalajs/ir/Trees.scala | 32 ++++ .../scala/org/scalajs/ir/PrintersTest.scala | 55 +++++++ .../scala/scala/scalajs/LinkingInfo.scala | 47 +++++- .../scalajs/annotation/linkTimeProperty.scala | 33 ++++ .../scalajs/linker/analyzer/Analyzer.scala | 2 +- .../scalajs/linker/analyzer/InfoLoader.scala | 39 +++-- .../org/scalajs/linker/analyzer/Infos.scala | 150 +++++++++++------- .../backend/wasmemitter/FunctionEmitter.scala | 3 +- .../linker/checker/ClassDefChecker.scala | 70 +++++++- .../scalajs/linker/checker/FeatureSet.scala | 6 +- .../scalajs/linker/checker/IRChecker.scala | 35 +++- .../scalajs/linker/frontend/BaseLinker.scala | 5 +- .../scalajs/linker/frontend/Desugarer.scala | 18 ++- .../linker/frontend/LinkTimeEvaluator.scala | 129 +++++++++++++++ .../org/scalajs/linker/frontend/Refiner.scala | 5 +- .../frontend/optimizer/OptimizerCore.scala | 3 +- .../org/scalajs/linker/AnalyzerTest.scala | 125 ++++++++++++++- .../org/scalajs/linker/IRCheckerTest.scala | 1 + .../linker/checker/ClassDefCheckerTest.scala | 78 +++++++++ .../LinkTimeEvaluatorTest.scala | 102 ++++++++++++ .../linker/testutils/TestIRBuilder.scala | 1 + .../testsuite/library/LinkTimeIfTest.scala | 95 +++++++++++ 32 files changed, 1178 insertions(+), 102 deletions(-) create mode 100644 compiler/src/test/scala/org/scalajs/nscplugin/test/LinkTimeIfTest.scala create mode 100644 library/src/main/scala/scala/scalajs/annotation/linkTimeProperty.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkTimeEvaluator.scala create mode 100644 linker/shared/src/test/scala/org/scalajs/linker/frontend/modulesplitter/LinkTimeEvaluatorTest.scala create mode 100644 test-suite/js/src/test/scala/org/scalajs/testsuite/library/LinkTimeIfTest.scala diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index e46b1dc14f..dc1348ea22 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -5511,6 +5511,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 +5539,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 diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala index 2b0c5590d9..58c4910233 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala @@ -135,10 +135,13 @@ trait JSDefinitions { 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..cf6f896453 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala @@ -71,7 +71,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 @@ -128,6 +129,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/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..9a05ed7788 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) 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..23a2eb7118 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 { 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..fd49eb406e 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( """ 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/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala index 22d3752fd4..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 @@ -50,7 +50,7 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean, 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] = { 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..00b40402fe 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 { 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..7cf164c228 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") } 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..3f87f8be04 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) @@ -609,7 +629,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 +813,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 +983,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 57f8eeb366..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 @@ -43,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)") @@ -149,6 +150,21 @@ private[linker] object Desugarer { 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 val (className, ctorName) = syntheticLambdaNamesFor(descriptor) 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/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/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 51cebcdcca..9f7fe1aa95 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 @@ -689,7 +689,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") } 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/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/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) + } +} From f0e7a337b03584b0bb2e7868153509fbd60a409b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 5 Jan 2025 19:19:20 +0100 Subject: [PATCH 08/36] Use JS bigint's if possible inside the `parseFloat` algorithm. We use a `linkTimeIf` to select a `bigint`-based implementation of `parseFloatDecimalCorrection` when they are supported. We need a `linkTimeIf` in this case because it uses the JS `**` operator, which does not link below ES 2016. The `bigint`-based implementation avoids bringing in the entire `BigInteger` implementation, which is a major code size win if that was the only reason `BigInteger` was needed. --- javalib/src/main/scala/java/lang/Float.scala | 82 ++++++++++++++++---- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/javalib/src/main/scala/java/lang/Float.scala b/javalib/src/main/scala/java/lang/Float.scala index 8fa4ce3070..a2d54c77fd 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 = { From 5c3184396b9a0fe83b10c2662093491d092df064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 16 May 2025 13:51:39 +0200 Subject: [PATCH 09/36] Refactoring: Isolate handling of javalib methods with special bodies. A number of methods from the javalib are special-cased by the compiler, which replaces their body with a dedicated `UnaryOp` or `BinaryOp`. This commit refactors that handling to isolate it better from the handling of regular methods. We also make it a bit more flexible, so that we can more easily add further such methods in the future. --- .../org/scalajs/nscplugin/GenJSCode.scala | 179 +++++++++++------- 1 file changed, 106 insertions(+), 73 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index dc1348ea22..6696ce90c2 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, @@ -7379,37 +7346,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 { @@ -7425,4 +7361,101 @@ private object GenJSCode { case _ => Some((tree, Nil)) } } + + 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} + 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.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.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 + } + } } From 7516f1abdb0f0529578867e46f1b083446b6d93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 19 May 2025 12:11:21 +0200 Subject: [PATCH 10/36] Switch to GitHub Actions for the Windows CI. AppVeyor has become increasingly unstable recently. While we're there, upgrade to Node.js 24.x. --- .github/workflows/windows-ci.yml | 51 ++++++++++++++++++++++++++++++++ appveyor.yml | 23 -------------- project/Build.scala | 9 ------ 3 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/windows-ci.yml delete mode 100644 appveyor.yml 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/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/project/Build.scala b/project/Build.scala index 80bfe9792c..a365701850 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( From e33a2129133a065a9de276b420104e76dc9197bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 22 Apr 2025 13:27:28 +0200 Subject: [PATCH 11/36] Use a DataView in FloatingPointBits. Previously, we used several typed arrays pointing to the same `ArrayBuffer`. Now, we use a single `DataView`. Since we can (must) force a specific endianness with `DataView`, we get rid of the `highOffset` and `lowOffset` fields, which further streamlines the code. The assembly produced by V8 is now better. Since it knows that it manipulates a single buffer, it needs fewer loads and type tests internally. I expect the same would be true for other optimizing engines. --- .../scala/java/lang/FloatingPointBits.scala | 114 +++++++----------- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- project/Build.scala | 6 +- project/NodeJSEnvForcePolyfills.scala | 1 + 4 files changed, 48 insertions(+), 79 deletions(-) diff --git a/javalib/src/main/scala/java/lang/FloatingPointBits.scala b/javalib/src/main/scala/java/lang/FloatingPointBits.scala index 96e1c8f64c..3b03975c3e 100644 --- a/javalib/src/main/scala/java/lang/FloatingPointBits.scala +++ b/javalib/src/main/scala/java/lang/FloatingPointBits.scala @@ -20,65 +20,33 @@ 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" - } - } - + /** Are typed arrays known to be supported at link time? + * + * If yes, we can dce polyfills away. + */ @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 def areTypedArraysKnownSupported: scala.Boolean = + scala.scalajs.LinkingInfo.esVersion >= ESVersion.ES2015 - 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 - } + /** The DataView we use when typed arrays are supported; null if they are not supported. + * + * We always use it in `littleEndian` mode. Major architectures are all + * little endian these days. + */ + private val dataView: typedarray.DataView = { + // If DataView exists, ArrayBuffer must exist as well. There is no need to test both. + if (areTypedArraysKnownSupported || js.typeOf(global.DataView) != "undefined") + new typedarray.DataView(new typedarray.ArrayBuffer(8)) + else + null } - 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 + if (areTypedArraysKnownSupported || (dataView != null)) null else makePowsOf2(len = 1 << 8, java.lang.Float.MIN_NORMAL.toDouble) private val doublePowsOf2: js.Array[scala.Double] = - if (areTypedArraysSupported) null + if (areTypedArraysKnownSupported || (dataView != null)) null else makePowsOf2(len = 1 << 11, java.lang.Double.MIN_NORMAL) private def makePowsOf2(len: Int, minNormal: scala.Double): js.Array[scala.Double] = { @@ -116,15 +84,11 @@ private[lang] object FloatingPointBits { /* 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) + val dataView = this.dataView // local copy + if (areTypedArraysKnownSupported || (dataView != null)) { + dataView.setFloat64(0, value, littleEndian = true) + dataView.getInt32(0, littleEndian = true) ^ dataView.getInt32(4, littleEndian = true) } else { doubleHashCodePolyfill(value) } @@ -136,38 +100,42 @@ private[lang] object FloatingPointBits { Long.hashCode(doubleToLongBitsPolyfillInline(value)) def intBitsToFloat(bits: Int): scala.Float = { - if (areTypedArraysSupported) { - int32Array(0) = bits - float32Array(0) + val dataView = this.dataView // local copy + if (areTypedArraysKnownSupported || (dataView != null)) { + dataView.setInt32(0, bits, littleEndian = true) + dataView.getFloat32(0, littleEndian = true) } else { intBitsToFloatPolyfill(bits).toFloat } } def floatToIntBits(value: scala.Float): Int = { - if (areTypedArraysSupported) { - float32Array(0) = value - int32Array(0) + val dataView = this.dataView // local copy + if (areTypedArraysKnownSupported || (dataView != null)) { + dataView.setFloat32(0, value, littleEndian = true) + dataView.getInt32(0, littleEndian = true) } else { floatToIntBitsPolyfill(value) } } def longBitsToDouble(bits: scala.Long): scala.Double = { - if (areTypedArraysSupported) { - int32Array(highOffset) = (bits >>> 32).toInt - int32Array(lowOffset) = bits.toInt - float64Array(0) + val dataView = this.dataView // local copy + if (areTypedArraysKnownSupported || (dataView != null)) { + dataView.setInt32(0, bits.toInt, littleEndian = true) + dataView.setInt32(4, (bits >>> 32).toInt, littleEndian = true) + dataView.getFloat64(0, littleEndian = true) } else { longBitsToDoublePolyfill(bits) } } def doubleToLongBits(value: scala.Double): scala.Long = { - if (areTypedArraysSupported) { - float64Array(0) = value - ((int32Array(highOffset).toLong << 32) | - (int32Array(lowOffset).toLong & 0xffffffffL)) + val dataView = this.dataView // local copy + if (areTypedArraysKnownSupported || (dataView != null)) { + dataView.setFloat64(0, value, littleEndian = true) + ((dataView.getInt32(0, littleEndian = true).toLong & 0xffffffffL) | + (dataView.getInt32(4, littleEndian = true).toLong << 32)) } else { doubleToLongBitsPolyfill(value) } 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..5b8f6dba4b 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 = 145795, + expectedFullLinkSizeWithoutClosure = 84996, + expectedFullLinkSizeWithClosure = 21364, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index a365701850..60a245b05b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2041,7 +2041,7 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 624000 to 625000, + fastLink = 623000 to 624000, fullLink = 96000 to 97000, fastLinkGz = 75000 to 79000, fullLinkGz = 25000 to 26000, @@ -2058,8 +2058,8 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 442000 to 443000, - fullLink = 93000 to 94000, + fastLink = 441000 to 442000, + fullLink = 92000 to 93000, fastLinkGz = 57000 to 58000, fullLinkGz = 25000 to 26000, )) 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; From f84fa2ed357bacbaa68acacff8110d088e838b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 10 May 2025 11:41:24 +0200 Subject: [PATCH 12/36] Add copies of the run/finally.scala partest into our own test suite. The compilation scheme for `finally` is a beast on Wasm. It's good to have direct feedback on it without having to wait for the full partest. --- .../testsuite/compiler/TryFinallyTest.scala | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/TryFinallyTest.scala 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..ffce227e01 --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/TryFinallyTest.scala @@ -0,0 +1,232 @@ +/* + * 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" + ) + } +} From a7b99e99bca75060c24b5972b65887767414e3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 16 May 2025 14:00:22 +0200 Subject: [PATCH 13/36] Handle System.identityHashCode as a method with a special body. Instead of having it call a primitive whose sole purpose was to be called from `System.identityHashCode()`. --- .../src/main/scala/org/scalajs/nscplugin/GenJSCode.scala | 8 +++----- .../main/scala/org/scalajs/nscplugin/JSDefinitions.scala | 1 - .../main/scala/org/scalajs/nscplugin/JSPrimitives.scala | 4 +--- javalib/src/main/scala/java/lang/System.scala | 2 +- .../src/main/scala/scala/scalajs/runtime/package.scala | 4 +++- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 6696ce90c2..e5b205179e 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -5254,11 +5254,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() @@ -7446,6 +7441,9 @@ private object GenJSCode { 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) ) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala index 58c4910233..e91b74d4ff 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala @@ -131,7 +131,6 @@ 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") diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala index cf6f896453..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 @@ -115,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) 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/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") From 12918186cb190c558a9fd1219352d59d4ae22594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 10 May 2025 12:27:06 +0200 Subject: [PATCH 14/36] Fix #5165: Use defaultable types for the locals of `TryFinally`s. As well as for the locals of `Labeled` blocks whose `Return`s cross a `try..finally` boundary. If their original type was not defaultable, we cast away nullability when reading them back. --- .../backend/wasmemitter/FunctionEmitter.scala | 26 ++++++++++-- .../linker/backend/webassembly/Types.scala | 19 ++++++++- .../testsuite/compiler/TryFinallyTest.scala | 40 +++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) 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 7cf164c228..7196e8a075 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 @@ -1040,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 @@ -3623,6 +3630,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 @@ -3850,7 +3862,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) @@ -3941,8 +3953,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 @@ -3959,7 +3974,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) @@ -4074,8 +4089,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/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/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 index ffce227e01..57d6119aa4 100644 --- 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 @@ -229,4 +229,44 @@ class TryFinallyTest { "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" + ) + } } From d97f4e43d83a4c9e62b9d3370081ec6a4d4d4645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 24 Apr 2025 15:26:08 +0200 Subject: [PATCH 15/36] Support reading jar files in the IR cleaner. That allows the IR cleaner to see the JS type definitions from the library when it appears as a jar on the classpath. This is the case for the linker private library. Adding that support will allow the linker private library to use JS type definitions from the Scala.js library, as long as the references can be erased away. --- project/Build.scala | 14 ++++++++++- project/JavalibIRCleaner.scala | 45 ++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 60a245b05b..3b57fe3a24 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -750,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", diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index be6103d72c..3ec8d081c8 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -10,7 +10,9 @@ 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 scala.collection.immutable.IndexedSeq import scala.collection.mutable @@ -53,7 +55,12 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } val jsTypes = { - val dependencyIR = dependencyFiles.iterator.map(readIR(_)) + val dependencyIR = dependencyFiles.iterator.flatMap { file => + if (file.getName().endsWith(".jar")) + readIRJar(file) + else + List(readIR(file)) + } val libIR = libIRMappings.iterator.map(_._1) getJSTypes(dependencyIR ++ libIR) } @@ -107,14 +114,42 @@ 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): List[ClassDef] = { + // Similar to PathIRContainer.JarIRContainer and its walkIR helper + + val classDefs = List.newBuilder[ClassDef] + + val dirVisitor = new SimpleFileVisitor[Path] { + override def visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult = { + if (path.getFileName().toString().endsWith(".sjsir")) + classDefs += readIR(path) + super.visitFile(path, attrs) + } + } + + // Open zip/jar file as filesystem. + // The type ascription is necessary on JDK 13+. + val fs = FileSystems.newFileSystem(jar.toPath(), null: ClassLoader) + try { + val iter = fs.getRootDirectories().iterator() + while (iter.hasNext()) + Files.walkFileTree(iter.next(), dirVisitor) + } finally { + fs.close() + } + + classDefs.result() + } + private def writeIRFile(file: File, tree: ClassDef): Unit = { Files.createDirectories(file.toPath().getParent()) val outputStream = From b49cff92db67e67c0cd509fba4f96d8b2dd1d3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 23 Apr 2025 22:11:37 +0200 Subject: [PATCH 16/36] Introduce IR UnaryOps for floating point bit manipulation. Previously, our floating point bit manipulation methods were implemented in user space. This commit instead introduces dedicated IR `UnaryOp`s for them. On Wasm, the implementations are straightforward opcodes. In JavaScript, we use the same strategies as before, but moved to the linker world. In most cases, we use a unique scratch `DataView` to perform the manipulations. It is now allocated as a `globalVar` in the emitter, rather than in user space. The functions for `Float`/`Int` conversions are straightforward, as well as those for `Double`/`Long` when we use `bigint`s for `Long`s. When we use `RuntimeLong`s, the conversions are implemented in `RuntimeLong`, but require the scratch `DataView` as an argument, which the linker injects. This new strategy brings several benefits: * For JavaScript, the generated code is basically optimal now. It produces tight sequences of Assembly instructions that do not allocate anything. * For Wasm, the implementation does not rely on JS interop anymore, even when the optimizer does not run. This property will be required when we target Wasm outside of a JS host. * We open a path to adding support for the "raw" variants `floatToRawIntBits`/`doubleToRawLongBits` in Wasm in the future, should we need it. --- .../org/scalajs/nscplugin/GenJSCode.scala | 10 +- .../main/scala/org/scalajs/ir/Printers.scala | 5 + .../src/main/scala/org/scalajs/ir/Trees.scala | 16 +- .../scala/org/scalajs/ir/PrintersTest.scala | 5 + javalib/src/main/scala/java/lang/Double.scala | 33 +- javalib/src/main/scala/java/lang/Float.scala | 6 +- .../scala/java/lang/FloatingPointBits.scala | 294 ------------------ .../runtime/FloatingPointBitsPolyfills.scala | 190 +++++++++++ .../scalajs/linker/runtime/RuntimeLong.scala | 28 ++ .../backend/emitter/PrivateLibHolder.scala | 2 + .../linker/backend/emitter/CoreJSLib.scala | 90 +++++- .../linker/backend/emitter/Emitter.scala | 10 + .../linker/backend/emitter/EmitterNames.scala | 8 + .../backend/emitter/FunctionEmitter.scala | 18 +- .../linker/backend/emitter/LongImpl.scala | 12 +- .../linker/backend/emitter/Transients.scala | 16 + .../linker/backend/emitter/VarField.scala | 8 + .../backend/wasmemitter/FunctionEmitter.scala | 40 +++ .../backend/wasmemitter/WasmTransients.scala | 18 +- .../scalajs/linker/checker/IRChecker.scala | 8 +- .../frontend/optimizer/OptimizerCore.scala | 100 +++--- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- project/Build.scala | 12 +- project/MiniLib.scala | 2 - 24 files changed, 539 insertions(+), 398 deletions(-) delete mode 100644 javalib/src/main/scala/java/lang/FloatingPointBits.scala create mode 100644 linker-private-library/src/main/scala/org/scalajs/linker/runtime/FloatingPointBitsPolyfills.scala diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 6696ce90c2..77c2e66d7f 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -7421,7 +7421,7 @@ private object GenJSCode { private lazy val JavalibMethodsWithOpBody: Map[(ClassName, MethodName), JavalibOpBody] = { import JavalibOpBody._ import js.{UnaryOp => unop, BinaryOp => binop} - import jstpe.{BooleanRef => Z, CharRef => C, IntRef => I} + import jstpe.{BooleanRef => Z, CharRef => C, IntRef => I, LongRef => J, FloatRef => F, DoubleRef => D} import MethodName.{apply => m} val O = jswkn.ObjectRef @@ -7429,6 +7429,14 @@ private object GenJSCode { val T = jstpe.ClassRef(jswkn.BoxedStringClass) val byClass: Map[ClassName, Map[MethodName, JavalibOpBody]] = Map( + 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) 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 9a05ed7788..c7f66671fd 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -445,6 +445,11 @@ 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 BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) => 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 23a2eb7118..f463279933 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -509,6 +509,14 @@ 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 + def isClassOp(op: Code): Boolean = op >= Class_name && op <= Class_superClass @@ -530,13 +538,13 @@ object Trees { case IntToShort => ShortType case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | - String_length | Array_length | IdentityHashCode => + String_length | Array_length | IdentityHashCode | Float_toBits => IntType - case IntToLong | DoubleToLong => + case IntToLong | DoubleToLong | Double_toBits => 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 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 fd49eb406e..6ce589756e 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -514,6 +514,11 @@ 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))) } @Test def printPseudoUnaryOp(): 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 a2d54c77fd..279d8ed1a8 100644 --- a/javalib/src/main/scala/java/lang/Float.scala +++ b/javalib/src/main/scala/java/lang/Float.scala @@ -427,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 3b03975c3e..0000000000 --- a/javalib/src/main/scala/java/lang/FloatingPointBits.scala +++ /dev/null @@ -1,294 +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 { - - /** Are typed arrays known to be supported at link time? - * - * If yes, we can dce polyfills away. - */ - @inline - private def areTypedArraysKnownSupported: scala.Boolean = - scala.scalajs.LinkingInfo.esVersion >= ESVersion.ES2015 - - /** The DataView we use when typed arrays are supported; null if they are not supported. - * - * We always use it in `littleEndian` mode. Major architectures are all - * little endian these days. - */ - private val dataView: typedarray.DataView = { - // If DataView exists, ArrayBuffer must exist as well. There is no need to test both. - if (areTypedArraysKnownSupported || js.typeOf(global.DataView) != "undefined") - new typedarray.DataView(new typedarray.ArrayBuffer(8)) - else - null - } - - private val floatPowsOf2: js.Array[scala.Double] = - if (areTypedArraysKnownSupported || (dataView != null)) null - else makePowsOf2(len = 1 << 8, java.lang.Float.MIN_NORMAL.toDouble) - - private val doublePowsOf2: js.Array[scala.Double] = - if (areTypedArraysKnownSupported || (dataView != null)) 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). - */ - val dataView = this.dataView // local copy - if (areTypedArraysKnownSupported || (dataView != null)) { - dataView.setFloat64(0, value, littleEndian = true) - dataView.getInt32(0, littleEndian = true) ^ dataView.getInt32(4, littleEndian = true) - } else { - doubleHashCodePolyfill(value) - } - } - } - - @noinline - private def doubleHashCodePolyfill(value: scala.Double): Int = - Long.hashCode(doubleToLongBitsPolyfillInline(value)) - - def intBitsToFloat(bits: Int): scala.Float = { - val dataView = this.dataView // local copy - if (areTypedArraysKnownSupported || (dataView != null)) { - dataView.setInt32(0, bits, littleEndian = true) - dataView.getFloat32(0, littleEndian = true) - } else { - intBitsToFloatPolyfill(bits).toFloat - } - } - - def floatToIntBits(value: scala.Float): Int = { - val dataView = this.dataView // local copy - if (areTypedArraysKnownSupported || (dataView != null)) { - dataView.setFloat32(0, value, littleEndian = true) - dataView.getInt32(0, littleEndian = true) - } else { - floatToIntBitsPolyfill(value) - } - } - - def longBitsToDouble(bits: scala.Long): scala.Double = { - val dataView = this.dataView // local copy - if (areTypedArraysKnownSupported || (dataView != null)) { - dataView.setInt32(0, bits.toInt, littleEndian = true) - dataView.setInt32(4, (bits >>> 32).toInt, littleEndian = true) - dataView.getFloat64(0, littleEndian = true) - } else { - longBitsToDoublePolyfill(bits) - } - } - - def doubleToLongBits(value: scala.Double): scala.Long = { - val dataView = this.dataView // local copy - if (areTypedArraysKnownSupported || (dataView != null)) { - dataView.setFloat64(0, value, littleEndian = true) - ((dataView.getInt32(0, littleEndian = true).toLong & 0xffffffffL) | - (dataView.getInt32(4, littleEndian = true).toLong << 32)) - } 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/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..7e4e256beb 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 @@ -541,6 +541,18 @@ final class RuntimeLong(val lo: Int, val hi: Int) { def remainderUnsigned(b: RuntimeLong): RuntimeLong = RuntimeLong.remainderUnsigned(a, b) + /** Computes `longBitsToDouble(this)`. + * + * `fpBitsDataView` must be a scratch `js.typedarray.DataView` whose + * underlying buffer is at least 8 bytes long. + */ + @inline + def bitsToDouble(fpBitsDataView: scala.scalajs.js.typedarray.DataView): Double = { + fpBitsDataView.setInt32(0, lo, littleEndian = true) + fpBitsDataView.setInt32(4, hi, littleEndian = true) + fpBitsDataView.getFloat64(0, littleEndian = true) + } + } object RuntimeLong { @@ -728,6 +740,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 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/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index 6fb37ce343..6b1eb65103 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") @@ -874,6 +876,48 @@ 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) => If(y === 0, throwDivByZero, { @@ -956,7 +1000,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(genLongModuleApply(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(genApply(x, LongImpl.bitsToDouble, 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..e96103fd08 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 @@ -1440,6 +1440,16 @@ object Emitter { callMethods(LongImpl.RuntimeLongClass, LongImpl.AllMethods.toList), callOnModule(LongImpl.RuntimeLongModuleClass, LongImpl.AllModuleMethods.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..2fbd0eecd0 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) => @@ -2515,6 +2516,16 @@ 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) } case BinaryOp(op, lhs, rhs) => @@ -2879,6 +2890,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..2c93e5e358 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 @@ -62,9 +62,10 @@ private[linker] object LongImpl { 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 toInt = MethodName("toInt", Nil, IntRef) + final val toFloat = MethodName("toFloat", Nil, FloatRef) final val toDouble = MethodName("toDouble", Nil, DoubleRef) + final val bitsToDouble = MethodName("bitsToDouble", List(ObjectRef), DoubleRef) final val byteValue = MethodName("byteValue", Nil, ByteRef) final val shortValue = MethodName("shortValue", Nil, ShortRef) @@ -81,7 +82,7 @@ private[linker] object LongImpl { private val OperatorMethods = Set( UNARY_-, UNARY_~, this.+, this.-, *, /, %, |, &, ^, <<, >>>, >>, - ===, !==, <, <=, >, >=, toInt, toFloat, toDouble) + ===, !==, <, <=, >, >=, toInt, toFloat, toDouble, bitsToDouble) private val BoxedLongMethods = Set( byteValue, shortValue, intValue, longValue, floatValue, doubleValue, @@ -107,11 +108,12 @@ private[linker] object LongImpl { // Methods on the companion - final val fromInt = MethodName("fromInt", List(IntRef), RTLongRef) + final val fromInt = MethodName("fromInt", List(IntRef), RTLongRef) final val fromDouble = MethodName("fromDouble", List(DoubleRef), RTLongRef) + final val fromDoubleBits = MethodName("fromDoubleBits", List(DoubleRef, ObjectRef), RTLongRef) val AllModuleMethods = Set( - fromInt, fromDouble) + fromInt, fromDouble, fromDoubleBits) // Extract the parts to give to the initFromParts constructor 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..ba3355e54a 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") @@ -270,6 +273,11 @@ private[emitter] object VarField { 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") 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 7cf164c228..2cf8d2682c 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 @@ -1638,6 +1638,46 @@ 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 } tree.tpe 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..3d3f12fdb8 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 @@ -62,11 +62,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 = { @@ -96,22 +91,17 @@ object WasmTransients { final val F64Nearest = 11 final val F64Sqrt = 12 - final val I32ReinterpretF32 = 13 - final val I64ReinterpretF64 = 14 - final val F32ReinterpretI32 = 15 - final val F64ReinterpretI64 = 16 - def resultTypeOf(op: Code): Type = (op: @switch) match { - case I32Clz | I32Ctz | I32Popcnt | I32ReinterpretF32 => + case I32Clz | I32Ctz | I32Popcnt => IntType - case I64Clz | I64Ctz | I64Popcnt | I64ReinterpretF64 => + case I64Clz | I64Ctz | I64Popcnt => LongType - case F32Abs | F32ReinterpretI32 => + case F32Abs => FloatType - case F64Abs | F64Ceil | F64Floor | F64Nearest | F64Sqrt | F64ReinterpretI64 => + case F64Abs | F64Ceil | F64Floor | F64Nearest | F64Sqrt => DoubleType } } 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 3f87f8be04..b744dd380a 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 @@ -538,13 +538,13 @@ private final class IRChecker(linkTimeProperties: LinkTimeProperties, ByteType case ShortToInt => ShortType - case IntToLong | IntToDouble | IntToChar | IntToByte | IntToShort => + case IntToLong | IntToDouble | IntToChar | IntToByte | IntToShort | Float_fromBits => IntType - case LongToInt | LongToDouble | LongToFloat => + case LongToInt | LongToDouble | LongToFloat | Double_fromBits => LongType - case FloatToDouble => + case FloatToDouble | Float_toBits => FloatType - case DoubleToInt | DoubleToFloat | DoubleToLong => + case DoubleToInt | DoubleToFloat | DoubleToLong | Double_toBits => DoubleType case String_length => StringType 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 9f7fe1aa95..422831e52e 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 @@ -1649,6 +1649,9 @@ private[optimizer] abstract class OptimizerCore( else Block(exprSideEffects, Transient(Cast(Null(), tpe))) + case Transient(GetFPBitsDataView) => + Skip()(stat.pos) + case _ => stat } @@ -1901,6 +1904,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 => @@ -2979,41 +2984,6 @@ private[optimizer] abstract class OptimizerCore( 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 => @@ -3586,12 +3556,12 @@ private[optimizer] abstract class OptimizerCore( def rtLongClassType = ClassType(LongImpl.RuntimeLongClass, nullable = true) def expandLongModuleOp(methodName: MethodName, - arg: PreTransform): TailRec[Tree] = { + args: 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, + args.toList, rtLongClassType, isStat = false, usePreTransform = true)( cont) } @@ -3631,6 +3601,15 @@ private[optimizer] abstract class OptimizerCore( case LongToFloat => expandUnaryOp(LongImpl.toFloat, arg, FloatType) + case Double_toBits if config.coreSpec.esFeatures.esVersion >= ESVersion.ES2015 => + expandLongModuleOp(LongImpl.fromDoubleBits, + arg, PreTransTree(Transient(GetFPBitsDataView))) + + case Double_fromBits if config.coreSpec.esFeatures.esVersion >= ESVersion.ES2015 => + // It's a bit of a hack to use expandBinaryOp here, but it's fine. + expandBinaryOp(LongImpl.bitsToDouble, + arg, PreTransTree(Transient(GetFPBitsDataView))) + case _ => cont(pretrans) } @@ -3940,6 +3919,37 @@ 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 + } + case _ => default } @@ -6492,13 +6502,7 @@ private[optimizer] object OptimizerCore { 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 = LongRemainderUnsigned + 1 final val StringCodePointAt = CharacterCodePointToString + 1 final val StringSubstringStart = StringCodePointAt + 1 @@ -6631,14 +6635,6 @@ private[optimizer] object OptimizerCore { 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 - ), ClassName("java.lang.Character$") -> List( m("toString", List(I), StringClassRef) -> CharacterCodePointToString ), 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 5b8f6dba4b..9fb1213758 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 = 145795, - expectedFullLinkSizeWithoutClosure = 84996, - expectedFullLinkSizeWithClosure = 21364, + expectedFastLinkSize = 146044, + expectedFullLinkSizeWithoutClosure = 85435, + expectedFullLinkSizeWithClosure = 21197, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index 3b57fe3a24..a383ebf1ac 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2053,16 +2053,16 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 623000 to 624000, + fastLink = 624000 to 625000, fullLink = 96000 to 97000, fastLinkGz = 75000 to 79000, fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 424000 to 425000, - fullLink = 281000 to 282000, - fastLinkGz = 60000 to 61000, + fastLink = 425000 to 426000, + fullLink = 282000 to 283000, + fastLinkGz = 61000 to 62000, fullLinkGz = 43000 to 44000, )) } @@ -2077,8 +2077,8 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 299000 to 300000, - fullLink = 257000 to 258000, + fastLink = 300000 to 301000, + fullLink = 258000 to 259000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, )) 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", From 6d0475759f17070655693d2c7cae8382ebe917fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 3 May 2025 13:41:11 +0200 Subject: [PATCH 17/36] Introduce IR BinaryOps for unsigned division and remainder. This allows to better mutualize their implementation with the signed divisions. Moreover, our 3 implementation strategies (JS with `RuntimeLong`, JS with `bigint` and Wasm) have different efficient implementations of those operations. Using IR BinaryOps for them allows each backend to use the most appropriate implementation, while letting the optimizer generically manipulate their mathematical properties. --- .../org/scalajs/nscplugin/GenJSCode.scala | 8 + .../main/scala/org/scalajs/ir/Printers.scala | 5 + .../src/main/scala/org/scalajs/ir/Trees.scala | 12 +- .../scala/org/scalajs/ir/PrintersTest.scala | 9 + .../src/main/scala/java/lang/Integer.scala | 8 +- javalib/src/main/scala/java/lang/Long.scala | 44 +---- .../org/scalajs/linker/analyzer/Infos.scala | 4 +- .../linker/backend/emitter/CoreJSLib.scala | 24 +-- .../backend/emitter/FunctionEmitter.scala | 66 +++---- .../linker/backend/emitter/LongImpl.scala | 13 +- .../linker/backend/emitter/VarField.scala | 8 +- .../backend/wasmemitter/FunctionEmitter.scala | 53 +++--- .../backend/wasmemitter/WasmTransients.scala | 12 +- .../scalajs/linker/checker/IRChecker.scala | 6 +- .../frontend/optimizer/OptimizerCore.scala | 166 +++++++++--------- .../org/scalajs/linker/LibrarySizeTest.scala | 4 +- project/Build.scala | 8 +- .../testsuite/javalib/lang/LongTest.scala | 18 +- 18 files changed, 221 insertions(+), 247 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 2fc15c4dd8..2c39124753 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -7424,6 +7424,14 @@ private object GenJSCode { val T = jstpe.ClassRef(jswkn.BoxedStringClass) val byClass: Map[ClassName, Map[MethodName, JavalibOpBody]] = Map( + jswkn.BoxedIntegerClass.withSuffix("$") -> Map( + m("divideUnsigned", List(I, I), I) -> ArgBinaryOp(binop.Int_unsigned_/), + m("remainderUnsigned", List(I, I), I) -> ArgBinaryOp(binop.Int_unsigned_%) + ), + jswkn.BoxedLongClass.withSuffix("$") -> Map( + m("divideUnsigned", List(J, J), J) -> ArgBinaryOp(binop.Long_unsigned_/), + m("remainderUnsigned", List(J, J), J) -> ArgBinaryOp(binop.Long_unsigned_%) + ), jswkn.BoxedFloatClass.withSuffix("$") -> Map( m("floatToIntBits", List(F), I) -> ArgUnaryOp(unop.Float_toBits), m("intBitsToFloat", List(I), F) -> ArgUnaryOp(unop.Float_fromBits) 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 c7f66671fd..facd69b122 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -576,6 +576,11 @@ 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]" }) print(' ') print(rhs) 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 f463279933..d0cc772980 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -679,6 +679,12 @@ 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 + def isClassOp(op: Code): Boolean = op >= Class_isInstance && op <= Class_newArray @@ -693,10 +699,12 @@ object Trees { 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 6ce589756e..d570445fc8 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -671,6 +671,15 @@ 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))) } @Test def printNewArray(): Unit = { diff --git a/javalib/src/main/scala/java/lang/Integer.scala b/javalib/src/main/scala/java/lang/Integer.scala index a4c2694365..0bf5d3561f 100644 --- a/javalib/src/main/scala/java/lang/Integer.scala +++ b/javalib/src/main/scala/java/lang/Integer.scala @@ -221,15 +221,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: diff --git a/javalib/src/main/scala/java/lang/Long.scala b/javalib/src/main/scala/java/lang/Long.scala index 0413372acf..faa69bc6d9 100644 --- a/javalib/src/main/scala/java/lang/Long.scala +++ b/javalib/src/main/scala/java/lang/Long.scala @@ -348,47 +348,11 @@ object Long { @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 - } + @inline def divideUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = + throw new Error("stub") // body replaced by the compiler back-end - if (isDivide) quot - else rem - } + @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 = { 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 00b40402fe..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 @@ -789,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 6b1eb65103..b2b64aee66 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 @@ -919,17 +919,11 @@ private[emitter] object CoreJSLib { } 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))) @@ -953,17 +947,11 @@ private[emitter] object CoreJSLib { } ) ::: condDefs(allowBigIntsForLongs && shouldDefineIntLongDivModFunctions)( - defineFunction2(VarField.longDiv) { (x, y) => + defineFunction1(VarField.checkLongDivisor) { y => If(y === bigInt(0), throwDivByZero, { - Return(wrapBigInt64(x / y)) + Return(y) }) - } ::: - defineFunction2(VarField.longMod) { (x, y) => - If(y === bigInt(0), throwDivByZero, { - Return(wrapBigInt64(x % y)) - }) - } ::: - Nil + } ) ::: condDefs(allowBigIntsForLongs)( defineFunction1(VarField.doubleToLong)(x => Return { 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 2fbd0eecd0..fc766eb527 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 @@ -1287,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 @@ -2206,6 +2208,9 @@ 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 = + js.BinaryOp(JSBinaryOp.>>>, tree, js.IntLiteral(0)) + def bigIntShiftRhs(tree: js.Tree): js.Tree = { tree match { case js.IntLiteral(v) => @@ -2631,20 +2636,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) @@ -2685,27 +2687,27 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { wrapBigInt64(js.BinaryOp(JSBinaryOp.*, newLhs, newRhs)) else genApply(newLhs, LongImpl.*, newRhs) - case Long_/ => + 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./ + case Long_% => LongImpl.% + case Long_unsigned_/ => LongImpl.divideUnsigned + case Long_unsigned_% => LongImpl.remainderUnsigned } - } else { - genApply(newLhs, LongImpl.%, newRhs) + genApply(newLhs, implMethodName, newRhs) } case Long_| => 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 2c93e5e358..1e1c6b8305 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 @@ -47,6 +47,9 @@ private[linker] object LongImpl { final val / = binaryOp("$div") final val % = binaryOp("$percent") + final val divideUnsigned = binaryOp("divideUnsigned") + final val remainderUnsigned = binaryOp("remainderUnsigned") + final val | = binaryOp("$bar") final val & = binaryOp("$amp") final val ^ = binaryOp("$up") @@ -81,8 +84,8 @@ private[linker] object LongImpl { final val compareToO = MethodName("compareTo", List(ClassRef(ObjectClass)), IntRef) private val OperatorMethods = Set( - UNARY_-, UNARY_~, this.+, this.-, *, /, %, |, &, ^, <<, >>>, >>, - ===, !==, <, <=, >, >=, toInt, toFloat, toDouble, bitsToDouble) + UNARY_-, UNARY_~, this.+, this.-, *, /, %, divideUnsigned, remainderUnsigned, + |, &, ^, <<, >>>, >>, ===, !==, <, <=, >, >=, toInt, toFloat, toDouble, bitsToDouble) private val BoxedLongMethods = Set( byteValue, shortValue, intValue, longValue, floatValue, doubleValue, @@ -92,12 +95,10 @@ private[linker] object LongImpl { // Methods used for intrinsics - final val compareToRTLong = MethodName("compareTo", List(RTLongRef), IntRef) - final val divideUnsigned = binaryOp("divideUnsigned") - final val remainderUnsigned = binaryOp("remainderUnsigned") + final val compareToRTLong = MethodName("compareTo", List(RTLongRef), IntRef) val AllIntrinsicMethods = Set( - compareToRTLong, divideUnsigned, remainderUnsigned) + compareToRTLong) // Constructors 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 ba3355e54a..98b3171e05 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 @@ -259,16 +259,12 @@ private[emitter] object VarField { // Arithmetic Call Helpers - final val intDiv = mk("$intDiv") + final val checkIntDivisor = mk("$checkIntDivisor") - final val intMod = mk("$intMod") + final val checkLongDivisor = mk("$checkLongDivisor") final val longToFloat = mk("$longToFloat") - final val longDiv = mk("$longDiv") - - final val longMod = mk("$longMod") - final val doubleToLong = mk("$doubleToLong") final val doubleToInt = mk("$doubleToInt") 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 21de9d63ed..7fbdeef29e 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 @@ -1734,33 +1734,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_<< => @@ -2136,7 +2137,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 @@ -2146,8 +2147,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 @@ -2156,7 +2156,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. */ @@ -2176,7 +2176,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 @@ -2185,8 +2185,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) @@ -2204,7 +2203,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)) @@ -2221,7 +2220,7 @@ private class FunctionEmitter private ( fb += mainOp } } else { - // lhs % rhs + // lhs mainOp rhs fb += wa.LocalGet(lhsLocal) fb += wa.LocalGet(rhsLocal) fb += mainOp 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 3d3f12fdb8..2ec686a081 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 @@ -137,13 +137,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 @@ -167,13 +163,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 @@ -187,10 +179,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/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index b744dd380a..c4e0ff7d68 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 @@ -571,11 +571,13 @@ private final class IRChecker(linkTimeProperties: LinkTimeProperties, 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_% => 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_% => LongType case Float_+ | Float_- | Float_* | Float_/ | Float_% => FloatType 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 422831e52e..2c0fa4ec70 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 @@ -1505,14 +1505,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 @@ -1838,8 +1838,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 @@ -2734,28 +2735,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 @@ -2897,13 +2876,6 @@ 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 => @@ -2961,29 +2933,6 @@ private[optimizer] abstract class OptimizerCore( 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.Character case CharacterCodePointToString => @@ -3647,6 +3596,9 @@ private[optimizer] abstract class OptimizerCore( case Long_> => expandBinaryOp(LongImpl.>, lhs, rhs) case Long_>= => expandBinaryOp(LongImpl.>=, lhs, rhs) + case Long_unsigned_/ => expandBinaryOp(LongImpl.divideUnsigned, lhs, rhs) + case Long_unsigned_% => expandBinaryOp(LongImpl.remainderUnsigned, lhs, rhs) + case _ => cont(pretrans) } @@ -4228,12 +4180,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)))) @@ -4258,6 +4206,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))) => @@ -4536,12 +4511,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)))) @@ -4558,10 +4529,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 => @@ -4586,6 +4557,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))) => @@ -5677,6 +5675,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) @@ -6489,20 +6493,16 @@ private[optimizer] object OptimizerCore { 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 LongNLZ = IntegerRotateRight + 1 final val LongNTZ = LongNLZ + 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 CharacterCodePointToString = LongRemainderUnsigned + 1 + final val CharacterCodePointToString = LongCompare + 1 final val StringCodePointAt = CharacterCodePointToString + 1 final val StringSubstringStart = StringCodePointAt + 1 @@ -6610,9 +6610,7 @@ 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 ) ) @@ -6622,18 +6620,14 @@ private[optimizer] object OptimizerCore { 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 + m("rotateRight", List(J, I), J) -> LongRotateRight ), ClassName("java.lang.Character$") -> List( m("toString", List(I), StringClassRef) -> CharacterCodePointToString 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 9fb1213758..4bbe92a6b0 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,8 +70,8 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 146044, - expectedFullLinkSizeWithoutClosure = 85435, + expectedFastLinkSize = 147727, + expectedFullLinkSizeWithoutClosure = 86377, expectedFullLinkSizeWithClosure = 21197, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/project/Build.scala b/project/Build.scala index a383ebf1ac..7209200769 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2053,7 +2053,7 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 624000 to 625000, + fastLink = 626000 to 627000, fullLink = 96000 to 97000, fastLinkGz = 75000 to 79000, fullLinkGz = 25000 to 26000, @@ -2061,7 +2061,7 @@ object Build { } else { Some(ExpectedSizes( fastLink = 425000 to 426000, - fullLink = 282000 to 283000, + fullLink = 283000 to 284000, fastLinkGz = 61000 to 62000, fullLinkGz = 43000 to 44000, )) @@ -2070,14 +2070,14 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 441000 to 442000, + fastLink = 443000 to 444000, fullLink = 92000 to 93000, fastLinkGz = 57000 to 58000, fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 300000 to 301000, + fastLink = 301000 to 302000, fullLink = 258000 to 259000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, 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..f9cb9c3e26 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)) @@ -659,8 +661,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 +727,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) From 8a885adaeb7f958bca74fdf908fbedc89c9be0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 19 May 2025 15:26:27 +0200 Subject: [PATCH 18/36] Move the require-jdk15 tests to require-jdk17. This way, we only have `require-jdk*` directories for JDK versions that we actually test in our CI. They also correspond to JDK LTS versions, so they are not arbitrary. --- project/Build.scala | 1 - .../scalajs/testsuite/javalib/io/InputStreamTestOnJDK17.scala} | 2 +- .../org/scalajs/testsuite/javalib/lang/ConstableTest.scala | 0 .../org/scalajs/testsuite/javalib/lang/ConstantDescTest.scala | 0 .../org/scalajs/testsuite/javalib/lang/StringTestOnJDK17.scala} | 2 +- 5 files changed, 2 insertions(+), 3 deletions(-) rename test-suite/shared/src/test/{require-jdk15/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK15.scala => require-jdk17/org/scalajs/testsuite/javalib/io/InputStreamTestOnJDK17.scala} (98%) rename test-suite/shared/src/test/{require-jdk15 => require-jdk17}/org/scalajs/testsuite/javalib/lang/ConstableTest.scala (100%) rename test-suite/shared/src/test/{require-jdk15 => require-jdk17}/org/scalajs/testsuite/javalib/lang/ConstantDescTest.scala (100%) rename test-suite/shared/src/test/{require-jdk15/org/scalajs/testsuite/javalib/lang/StringTestOnJDK15.scala => require-jdk17/org/scalajs/testsuite/javalib/lang/StringTestOnJDK17.scala} (99%) diff --git a/project/Build.scala b/project/Build.scala index 7209200769..c1337508a2 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2138,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/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 From 4c128da9a1fd25c2776a8d0be57f17117026bc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 19 May 2025 17:13:58 +0200 Subject: [PATCH 19/36] Implement jl.Math.{multiplyFull, multiplyHigh, unsignedMultiplyHigh}. --- javalib/src/main/scala/java/lang/Math.scala | 37 +++ .../scalajs/linker/runtime/RuntimeLong.scala | 41 +++ .../linker/backend/emitter/LongImpl.scala | 7 + .../frontend/optimizer/IncOptimizer.scala | 5 +- .../frontend/optimizer/OptimizerCore.scala | 40 ++- .../javalib/lang/MathTestOnJDK11.scala | 242 ++++++++++++++++++ .../javalib/lang/MathTestOnJDK21.scala | 129 ++++++++++ 7 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/lang/MathTestOnJDK11.scala create mode 100644 test-suite/shared/src/test/require-jdk21/org/scalajs/testsuite/javalib/lang/MathTestOnJDK21.scala 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/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 7e4e256beb..f570defbe6 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 @@ -767,6 +767,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) 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 1e1c6b8305..b554c83d8a 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 @@ -116,6 +116,13 @@ private[linker] object LongImpl { val AllModuleMethods = Set( fromInt, fromDouble, fromDoubleBits) + // Methods on the companion used for intrinsics + + final val multiplyFull = MethodName("multiplyFull", List(IntRef, IntRef), RTLongRef) + + val AllIntrinsicModuleMethods = Set( + multiplyFull) + // 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/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 38bdb804c3..7aea1cd466 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,10 @@ 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) + multiple( + callMethods(LongImpl.RuntimeLongClass, LongImpl.AllIntrinsicMethods.toList), + callMethods(LongImpl.RuntimeLongModuleClass, LongImpl.AllIntrinsicModuleMethods.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 2c0fa4ec70..93a8cbbdfc 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 @@ -2993,6 +2993,32 @@ 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] = { + import LongImpl.{RuntimeLongModuleClass => modCls} + val receiver = + makeCast(LoadModule(modCls), ClassType(modCls, nullable = false)).toPreTransform + + pretransformApply(ApplyFlags.empty, + receiver, + 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 => @@ -4374,6 +4400,14 @@ private[optimizer] abstract class OptimizerCore( 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) @@ -6518,8 +6552,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 @@ -6611,6 +6646,9 @@ private[optimizer] object OptimizerCore { ClassName("java.lang.Long$") -> List( m("toString", List(J), ClassRef(BoxedStringClass)) -> LongToString, m("compare", List(J, J), I) -> LongCompare + ), + ClassName("java.lang.Math$") -> List( + m("multiplyFull", List(I, I), J) -> MathMultiplyFull ) ) 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-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) + } + +} From 303de0d88cabf30d6b1bde6f7f6615699acbfb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 21 May 2025 14:16:09 +0200 Subject: [PATCH 20/36] Optimize away comparisons of a variable with itself. --- .../linker/frontend/optimizer/OptimizerCore.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 2c0fa4ec70..1dba903d04 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 @@ -4401,6 +4401,9 @@ 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 @@ -4452,6 +4455,9 @@ private[optimizer] abstract class OptimizerCore( case _ => default } + case (PreTransLocalDef(l), PreTransLocalDef(r)) if l eq r => + booleanLit(op == Int_<= || op == Int_>=) + case (PreTransLit(IntLiteral(_)), _) => foldBinaryOp(flippedOp, rhs, lhs) @@ -4695,6 +4701,9 @@ 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 @@ -4818,6 +4827,9 @@ private[optimizer] abstract class OptimizerCore( } (finishTransform(isStat = false))(emptyScope) }.toPreTransform + case (PreTransLocalDef(l), PreTransLocalDef(r)) if l eq r => + booleanLit(op == Long_<= || op == Long_>=) + case (PreTransLit(LongLiteral(_)), _) => foldBinaryOp(flippedOp, rhs, lhs) From 52668c3be62db2136854da6ceee8be74400b5ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 22 May 2025 16:34:01 +0200 Subject: [PATCH 21/36] Opt: Rewrite `1 + ~x` to `-x`. --- .../org/scalajs/linker/frontend/optimizer/OptimizerCore.scala | 4 ++++ 1 file changed, 4 insertions(+) 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 1dba903d04..6fa8df30ff 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 @@ -4141,6 +4141,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 } From 4e32fa76e11998ee889273116b12ae5e9b95be24 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 23 May 2025 17:44:38 +0300 Subject: [PATCH 22/36] Use Java Streams for lazy Jar traversal in IRCleaner --- project/JavalibIRCleaner.scala | 54 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 3ec8d081c8..ea61e9e7b3 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -13,9 +13,11 @@ import java.net.URI 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} @@ -55,14 +57,16 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } val jsTypes = { - val dependencyIR = dependencyFiles.iterator.flatMap { file => - if (file.getName().endsWith(".jar")) - readIRJar(file) - else - List(readIR(file)) + val dependencyIR: Stream[ClassDef] = { + dependencyFiles.asJava.stream().flatMap { file => + if (file.getName().endsWith(".jar")) + readIRJar(file) + else + Stream.of[ClassDef](readIR(file)) + } } - val libIR = libIRMappings.iterator.map(_._1) - getJSTypes(dependencyIR ++ libIR) + val libIR: Stream[ClassDef] = libIRMappings.asJava.stream().map(_._1) + getJSTypes(Stream.concat(dependencyIR, libIR)) } val resultBuilder = Set.newBuilder[File] @@ -123,31 +127,21 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { Serializers.deserialize(buffer) } - private def readIRJar(jar: File): List[ClassDef] = { - // Similar to PathIRContainer.JarIRContainer and its walkIR helper - - val classDefs = List.newBuilder[ClassDef] - - val dirVisitor = new SimpleFileVisitor[Path] { - override def visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult = { - if (path.getFileName().toString().endsWith(".sjsir")) - classDefs += readIR(path) - super.visitFile(path, attrs) - } + 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) - try { - val iter = fs.getRootDirectories().iterator() - while (iter.hasNext()) - Files.walkFileTree(iter.next(), dirVisitor) - } finally { - fs.close() - } - - classDefs.result() + 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 = { @@ -161,8 +155,10 @@ 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]): Map[ClassName, ClassDef] = { + trees.filter(_.kind.isJSType).reduce[Map[ClassName, ClassDef]]( + Map.empty, (m, v) => m.updated(v.className, v), _ ++ _) + } private def cleanTree(tree: ClassDef, jsTypes: Map[ClassName, ClassDef], errorManager: ErrorManager): ClassDef = { From 8a352050f07d77d9a7fc68781e1fa15c3b85ad96 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 23 May 2025 18:07:16 +0300 Subject: [PATCH 23/36] Do not retain the entire ClassDef in the IRCleaner We only need the load spec, this allows memory to be freed up more eagerly. --- project/JavalibIRCleaner.scala | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index ea61e9e7b3..f2f6f6a046 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -45,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] = { @@ -155,19 +157,19 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } } - private def getJSTypes(trees: Stream[ClassDef]): Map[ClassName, ClassDef] = { - trees.filter(_.kind.isJSType).reduce[Map[ClassName, ClassDef]]( - Map.empty, (m, v) => m.updated(v.className, v), _ ++ _) + 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 = { @@ -496,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") From a5337ed336dcd831de330647f63048f4df6dda0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 25 May 2025 17:52:44 +0200 Subject: [PATCH 24/36] Opt: Fewer (and more predictable) branches in Range.isEmpty. Extract branches to be mostly dependent on `isInclusive` and `step >= 0`. These are often constant, and when they're not statically constant, they are probably predictable anyway. --- .../scala/collection/immutable/Range.scala | 13 +++++++------ .../scala/collection/immutable/Range.scala | 9 +++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/scalalib/overrides-2.12/scala/collection/immutable/Range.scala b/scalalib/overrides-2.12/scala/collection/immutable/Range.scala index e5f4287a76..80558680d8 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)`, @@ -78,9 +78,10 @@ extends scala.collection.AbstractSeq[Int] // 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) ) private val numRangeElements: Int = { @@ -194,7 +195,7 @@ extends scala.collection.AbstractSeq[Int] copy(locationAfterN(n), end, step) } ) - + /** Creates a new range containing the elements starting at `from` up to but not including `until`. * * $doesNotUseBuilders @@ -211,7 +212,7 @@ extends scala.collection.AbstractSeq[Int] 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 diff --git a/scalalib/overrides-2.13/scala/collection/immutable/Range.scala b/scalalib/overrides-2.13/scala/collection/immutable/Range.scala index 310c45c079..5381ebea9f 100644 --- a/scalalib/overrides-2.13/scala/collection/immutable/Range.scala +++ b/scalalib/overrides-2.13/scala/collection/immutable/Range.scala @@ -89,10 +89,11 @@ sealed abstract class Range( 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) + ) private[this] val numRangeElements: Int = { if (step == 0) throw new IllegalArgumentException("step cannot be 0.") From d9722185beae7b66772b3bf9f91894a781613fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 21 May 2025 14:16:34 +0200 Subject: [PATCH 25/36] Opt: Use unsigned arithmetics in Range, instead of Longs. Previously, `Range` used a number of intermediate operations on `Long`s to avoid overflow. The optimizer was already good enough to remove them in many cases, in particular when the step is `1` and/or the start is `0`, which are common cases. Now that the optimizer can reason about unsigned division, we can do a lot better. We can entirely avoid `Long` computations, *and* streamline a lot of code, by using unsigned `Int` arithmetics. This produces much better code for the cases where the optimizer cannot entirely get rid of the computations. --- project/Build.scala | 22 +- .../scala/collection/immutable/Range.scala | 231 +++++++++++------- .../scala/collection/immutable/Range.scala | 218 +++++++++++------ 3 files changed, 295 insertions(+), 176 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 7209200769..196b1a9939 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2053,16 +2053,16 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 626000 to 627000, - fullLink = 96000 to 97000, + fastLink = 624000 to 625000, + fullLink = 94000 to 95000, fastLinkGz = 75000 to 79000, - fullLinkGz = 25000 to 26000, + fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 425000 to 426000, - fullLink = 283000 to 284000, - fastLinkGz = 61000 to 62000, + fastLink = 424000 to 425000, + fullLink = 281000 to 282000, + fastLinkGz = 60000 to 61000, fullLinkGz = 43000 to 44000, )) } @@ -2070,15 +2070,15 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 443000 to 444000, - fullLink = 92000 to 93000, + fastLink = 442000 to 443000, + fullLink = 90000 to 91000, fastLinkGz = 57000 to 58000, - fullLinkGz = 25000 to 26000, + fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 301000 to 302000, - fullLink = 258000 to 259000, + fastLink = 299000 to 300000, + fullLink = 257000 to 258000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, )) diff --git a/scalalib/overrides-2.12/scala/collection/immutable/Range.scala b/scalalib/overrides-2.12/scala/collection/immutable/Range.scala index 80558680d8..c4d5f9cdf1 100644 --- a/scalalib/overrides-2.12/scala/collection/immutable/Range.scala +++ b/scalalib/overrides-2.12/scala/collection/immutable/Range.scala @@ -67,16 +67,6 @@ 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 = ( if (isInclusive) (if (step >= 0) start > end else start < end) @@ -84,25 +74,85 @@ extends scala.collection.AbstractSeq[Int] (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 @@ -135,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) { @@ -162,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 @@ -169,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. * @@ -186,15 +253,11 @@ 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`. * @@ -204,14 +267,16 @@ 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. * @@ -250,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 } @@ -300,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. @@ -316,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. @@ -341,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) } } @@ -399,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" } } @@ -433,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 5381ebea9f..6e3130a886 100644 --- a/scalalib/overrides-2.13/scala/collection/immutable/Range.scala +++ b/scalalib/overrides-2.13/scala/collection/immutable/Range.scala @@ -81,11 +81,6 @@ 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 = ( @@ -95,27 +90,90 @@ sealed abstract class Range( (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 @@ -169,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 = { @@ -226,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. @@ -275,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 @@ -335,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 @@ -370,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) } } @@ -479,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" } @@ -550,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 = From f19a18390866bbf04242a7e96b984e99661c2e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 23 May 2025 14:26:29 +0200 Subject: [PATCH 26/36] Use static methods as entry points for RuntimeLong operator methods. Previously, we used instance methods on `RuntimeLong` to implement the operators whose lhs is a `RuntimeLong`. Other operators, such as `fromDouble`, were implemented as methods of the module. Now, we use static methods for all the operators. Once inline, it does not make any difference. However, it leaves behind a few unused static methods, instead of some instance methods. The former can be removed by off-the-shelf JS minifies, unlike the latter. Paradoxically, the `Minify` output, as we measure it, gets bigger. That's because static method names are not renamed by the name minifier. --- .../scalajs/linker/runtime/RuntimeLong.scala | 184 ++++++++---------- .../linker/backend/emitter/CoreJSLib.scala | 4 +- .../linker/backend/emitter/Emitter.scala | 4 +- .../backend/emitter/FunctionEmitter.scala | 50 ++--- .../linker/backend/emitter/LongImpl.scala | 137 ++++++------- .../linker/backend/emitter/NameGen.scala | 2 +- .../linker/backend/emitter/SJSGen.scala | 7 +- .../frontend/optimizer/IncOptimizer.scala | 5 +- .../frontend/optimizer/OptimizerCore.scala | 108 +++++----- .../linker/standard/SymbolRequirement.scala | 5 + .../org/scalajs/linker/LibrarySizeTest.scala | 4 +- project/Build.scala | 8 +- 12 files changed, 251 insertions(+), 267 deletions(-) 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 f570defbe6..a0148560ed 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,81 @@ 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 + + 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 = _ // 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) - - @inline - def notEquals(b: RuntimeLong): Boolean = - !inline_equals(b) + def notEquals(a: RuntimeLong, b: RuntimeLong): Boolean = + !equals(a, 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 +128,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 +139,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 +150,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. */ @@ -156,26 +163,26 @@ final class RuntimeLong(val lo: Int, val hi: Int) { // Bitwise operations @inline - def unary_~ : RuntimeLong = // scalastyle:ignore - new RuntimeLong(~lo, ~hi) + def not(a: RuntimeLong): RuntimeLong = + new RuntimeLong(~a.lo, ~a.hi) @inline - def |(b: RuntimeLong): RuntimeLong = + 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,43 +248,43 @@ 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 + def neg(a: RuntimeLong): RuntimeLong = { + val lo = a.lo + val hi = a.hi new RuntimeLong(inline_lo_unary_-(lo), inline_hi_unary_-(lo, hi)) } @inline - def +(b: RuntimeLong): RuntimeLong = { + def add(a: RuntimeLong, b: RuntimeLong): RuntimeLong = { val alo = a.lo val ahi = a.hi val bhi = b.hi @@ -287,7 +294,7 @@ final class RuntimeLong(val lo: Int, val hi: Int) { } @inline - def -(b: RuntimeLong): RuntimeLong = { + def sub(a: RuntimeLong, b: RuntimeLong): RuntimeLong = { val alo = a.lo val ahi = a.hi val bhi = b.hi @@ -297,7 +304,7 @@ final class RuntimeLong(val lo: Int, val hi: Int) { } @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,54 +530,23 @@ final class RuntimeLong(val lo: Int, val hi: Int) { new RuntimeLong(lo, hi) } - @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) - - @inline - def %(b: RuntimeLong): RuntimeLong = - RuntimeLong.remainder(a, b) - - /** `java.lang.Long.remainderUnsigned(a, b)` */ - @inline - def remainderUnsigned(b: RuntimeLong): RuntimeLong = - RuntimeLong.remainderUnsigned(a, b) - - /** Computes `longBitsToDouble(this)`. + /** Computes `longBitsToDouble(a)`. * * `fpBitsDataView` must be a scratch `js.typedarray.DataView` whose * underlying buffer is at least 8 bytes long. */ @inline - def bitsToDouble(fpBitsDataView: scala.scalajs.js.typedarray.DataView): Double = { - fpBitsDataView.setInt32(0, lo, littleEndian = true) - fpBitsDataView.setInt32(4, hi, littleEndian = true) + def bitsToDouble(a: RuntimeLong, + fpBitsDataView: scala.scalajs.js.typedarray.DataView): Double = { + + fpBitsDataView.setInt32(0, a.lo, littleEndian = true) + fpBitsDataView.setInt32(4, a.hi, littleEndian = true) fpBitsDataView.getFloat64(0, littleEndian = true) } -} - -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 = _ + @inline + def toString(a: RuntimeLong): String = + toString(a.lo, a.hi) private def toString(lo: Int, hi: Int): String = { if (isInt32(lo, hi)) { @@ -608,6 +584,14 @@ object RuntimeLong { } } + @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 @@ -618,6 +602,10 @@ object RuntimeLong { } } + @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 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 b2b64aee66..e1568b7429 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 @@ -1020,7 +1020,7 @@ private[emitter] object CoreJSLib { Return(Apply(genIdentBracketSelect(fpBitsDataView, "getBigInt64"), List(0, bool(true)))) ) } else { - Return(genLongModuleApply(LongImpl.fromDoubleBits, x, fpBitsDataView)) + Return(genLongApplyStatic(LongImpl.fromDoubleBits, x, fpBitsDataView)) } } ::: defineFloatingPointBitsFunctionOrPolyfill(VarField.doubleFromBits, doubleFromBits) { (x, fpBitsDataView) => @@ -1030,7 +1030,7 @@ private[emitter] object CoreJSLib { Return(Apply(genIdentBracketSelect(fpBitsDataView, "getFloat64"), List(0, bool(true)))) ) } else { - Return(genApply(x, LongImpl.bitsToDouble, fpBitsDataView)) + Return(genLongApplyStatic(LongImpl.bitsToDouble, x, fpBitsDataView)) } } } 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 e96103fd08..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,8 @@ 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) ) }, 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 fc766eb527..cce354512e 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 @@ -2395,7 +2395,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 => @@ -2412,7 +2412,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 => @@ -2423,19 +2423,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 => @@ -2668,25 +2668,25 @@ 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) wrapBigInt64(js.UnaryOp(JSUnaryOp.-, newRhs)) else - genApply(newRhs, LongImpl.UNARY_-) + genLongApplyStatic(LongImpl.neg, newRhs) case _ => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs)) else - genApply(newLhs, LongImpl.-, newRhs) + genLongApplyStatic(LongImpl.sub, newLhs, newRhs) } case Long_* => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.*, newLhs, newRhs)) else - genApply(newLhs, LongImpl.*, newRhs) + genLongApplyStatic(LongImpl.mul, newLhs, newRhs) case Long_/ | Long_% | Long_unsigned_/ | Long_unsigned_% => if (useBigIntForLongs) { val newRhs1 = rhs match { @@ -2702,83 +2702,83 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } else { // The zero divisor check is performed by the implementation methods val implMethodName = (op: @switch) match { - case Long_/ => LongImpl./ - case Long_% => LongImpl.% + case Long_/ => LongImpl.divide + case Long_% => LongImpl.remainder case Long_unsigned_/ => LongImpl.divideUnsigned case Long_unsigned_% => LongImpl.remainderUnsigned } - genApply(newLhs, implMethodName, 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) wrapBigInt64(js.UnaryOp(JSUnaryOp.~, newRhs)) else - genApply(newRhs, LongImpl.UNARY_~) + genLongApplyStatic(LongImpl.not, newRhs) case _ => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.^, newLhs, newRhs)) else - genApply(newLhs, LongImpl.^, newRhs) + 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)) 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 b554c83d8a..f7f5e09f10 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,110 +18,111 @@ 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) + MethodName(name, TwoRTLongRefs, BooleanRef) - final val UNARY_- = unaryOp("unary_$minus") - final val UNARY_~ = unaryOp("unary_$tilde") + // Instance methods that we need to reach as part of the jl.Long boxing - final val + = binaryOp("$plus") - final val - = binaryOp("$minus") - final val * = binaryOp("$times") - final val / = binaryOp("$div") - final val % = binaryOp("$percent") + 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) - final val divideUnsigned = binaryOp("divideUnsigned") - final val remainderUnsigned = binaryOp("remainderUnsigned") + 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) - 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 bitsToDouble = MethodName("bitsToDouble", List(ObjectRef), 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.-, *, /, %, divideUnsigned, remainderUnsigned, - |, &, ^, <<, >>>, >>, ===, !==, <, <=, >, >=, toInt, toFloat, toDouble, bitsToDouble) - - private val BoxedLongMethods = Set( + 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 neg = unaryOp("neg") + final val not = unaryOp("not") - final val compareToRTLong = MethodName("compareTo", List(RTLongRef), IntRef) + 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") - val AllIntrinsicMethods = Set( - compareToRTLong) + final val divideUnsigned = binaryOp("divideUnsigned") + final val remainderUnsigned = binaryOp("remainderUnsigned") - // Constructors + final val or = binaryOp("or") + final val and = binaryOp("and") + final val xor = binaryOp("xor") - final val initFromParts = MethodName.constructor(List(IntRef, IntRef)) + final val shl = shiftOp("shl") + final val shr = shiftOp("shr") + final val sar = shiftOp("sar") - val AllConstructors = Set( - initFromParts) + 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") - // Methods on the companion + 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 fromInt = MethodName("fromInt", List(IntRef), RTLongRef) final val fromDouble = MethodName("fromDouble", List(DoubleRef), RTLongRef) final val fromDoubleBits = MethodName("fromDoubleBits", List(DoubleRef, ObjectRef), RTLongRef) - val AllModuleMethods = Set( - fromInt, fromDouble, fromDoubleBits) + val OperatorMethods = Set( + neg, not, + add, sub, mul, + divide, remainder, divideUnsigned, remainderUnsigned, + or, and, xor, shl, shr, sar, + equals_, notEquals, lt, le, gt, ge, + toInt, toFloat, toDouble, bitsToDouble, fromInt, fromDouble, fromDoubleBits + ) - // Methods on the companion used for intrinsics + // 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 AllIntrinsicModuleMethods = Set( - multiplyFull) + val AllIntrinsicMethods = Set( + toString_, + compare, + multiplyFull + ) + + // Constructors + + final val initFromParts = MethodName.constructor(List(IntRef, IntRef)) + + val AllConstructors = Set( + initFromParts) // Extract the parts to give to the initFromParts constructor 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/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/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 7aea1cd466..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,10 +75,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: multiple( cond(!targetIsWebAssembly && !esFeatures.allowBigIntsForLongs) { // Required by the intrinsics manipulating Longs - multiple( - callMethods(LongImpl.RuntimeLongClass, LongImpl.AllIntrinsicMethods.toList), - callMethods(LongImpl.RuntimeLongModuleClass, LongImpl.AllIntrinsicModuleMethods.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 b0033a4d16..2f0011e32e 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 @@ -2220,18 +2220,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))) } } @@ -2923,13 +2933,13 @@ 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) @@ -2995,12 +3005,8 @@ private[optimizer] abstract class OptimizerCore( case MathMultiplyFull => def expand(targs: List[PreTransform]): TailRec[Tree] = { - import LongImpl.{RuntimeLongModuleClass => modCls} - val receiver = - makeCast(LoadModule(modCls), ClassType(modCls, nullable = false)).toPreTransform - - pretransformApply(ApplyFlags.empty, - receiver, + pretransformApplyStatic(ApplyFlags.empty, + LongImpl.RuntimeLongClass, MethodIdent(LongImpl.multiplyFull), targs, ClassType(LongImpl.RuntimeLongClass, nullable = true), @@ -3530,29 +3536,20 @@ private[optimizer] abstract class OptimizerCore( // unfortunately nullable for the result types of methods def rtLongClassType = ClassType(LongImpl.RuntimeLongClass, nullable = true) - def expandLongModuleOp(methodName: MethodName, - args: PreTransform*): TailRec[Tree] = { - import LongImpl.{RuntimeLongModuleClass => modCls} - val receiver = - makeCast(LoadModule(modCls), ClassType(modCls, nullable = false)).toPreTransform - pretransformApply(ApplyFlags.empty, receiver, MethodIdent(methodName), - args.toList, 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)( + pretransformApplyStatic(ApplyFlags.empty, LongImpl.RuntimeLongClass, + MethodIdent(methodName), arg :: 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)( + pretransformApplyStatic(ApplyFlags.empty, LongImpl.RuntimeLongClass, + MethodIdent(methodName), lhs :: rhs :: Nil, resultType, + isStat = false, usePreTransform = true)( cont) } @@ -3562,7 +3559,7 @@ private[optimizer] abstract class OptimizerCore( (op: @switch) match { case IntToLong => - expandLongModuleOp(LongImpl.fromInt, arg) + expandUnaryOp(LongImpl.fromInt, arg) case LongToInt => expandUnaryOp(LongImpl.toInt, arg, IntType) @@ -3571,17 +3568,16 @@ private[optimizer] abstract class OptimizerCore( expandUnaryOp(LongImpl.toDouble, arg, DoubleType) case DoubleToLong => - expandLongModuleOp(LongImpl.fromDouble, arg) + expandUnaryOp(LongImpl.fromDouble, arg) case LongToFloat => expandUnaryOp(LongImpl.toFloat, arg, FloatType) case Double_toBits if config.coreSpec.esFeatures.esVersion >= ESVersion.ES2015 => - expandLongModuleOp(LongImpl.fromDoubleBits, + expandBinaryOp(LongImpl.fromDoubleBits, arg, PreTransTree(Transient(GetFPBitsDataView))) case Double_fromBits if config.coreSpec.esFeatures.esVersion >= ESVersion.ES2015 => - // It's a bit of a hack to use expandBinaryOp here, but it's fine. expandBinaryOp(LongImpl.bitsToDouble, arg, PreTransTree(Transient(GetFPBitsDataView))) @@ -3593,34 +3589,34 @@ private[optimizer] abstract class OptimizerCore( import BinaryOp._ (op: @switch) match { - case Long_+ => expandBinaryOp(LongImpl.+, lhs, rhs) + case Long_+ => expandBinaryOp(LongImpl.add, lhs, rhs) case Long_- => lhs match { case PreTransLit(LongLiteral(0L)) => - expandUnaryOp(LongImpl.UNARY_-, rhs) + expandUnaryOp(LongImpl.neg, rhs) case _ => - expandBinaryOp(LongImpl.-, lhs, rhs) + expandBinaryOp(LongImpl.sub, lhs, rhs) } - case Long_* => expandBinaryOp(LongImpl.*, lhs, rhs) - case Long_/ => expandBinaryOp(LongImpl./, lhs, rhs) - case Long_% => expandBinaryOp(LongImpl.%, lhs, rhs) + case Long_* => expandBinaryOp(LongImpl.mul, lhs, rhs) + case Long_/ => expandBinaryOp(LongImpl.divide, lhs, rhs) + case Long_% => expandBinaryOp(LongImpl.remainder, lhs, rhs) - case Long_& => expandBinaryOp(LongImpl.&, lhs, rhs) - case Long_| => expandBinaryOp(LongImpl.|, lhs, rhs) - case Long_^ => expandBinaryOp(LongImpl.^, lhs, rhs) + case Long_& => expandBinaryOp(LongImpl.and, lhs, rhs) + case Long_| => expandBinaryOp(LongImpl.or, lhs, rhs) + case Long_^ => expandBinaryOp(LongImpl.xor, lhs, rhs) - case Long_<< => expandBinaryOp(LongImpl.<<, lhs, rhs) - case Long_>>> => expandBinaryOp(LongImpl.>>>, lhs, rhs) - case Long_>> => expandBinaryOp(LongImpl.>>, lhs, rhs) + case Long_<< => expandBinaryOp(LongImpl.shl, lhs, rhs) + case Long_>>> => expandBinaryOp(LongImpl.shr, lhs, rhs) + case Long_>> => expandBinaryOp(LongImpl.sar, 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.equals_, lhs, rhs) + case Long_!= => expandBinaryOp(LongImpl.notEquals, lhs, rhs) + case Long_< => expandBinaryOp(LongImpl.lt, lhs, rhs) + case Long_<= => expandBinaryOp(LongImpl.le, lhs, rhs) + case Long_> => expandBinaryOp(LongImpl.gt, lhs, rhs) + case Long_>= => expandBinaryOp(LongImpl.ge, lhs, rhs) case Long_unsigned_/ => expandBinaryOp(LongImpl.divideUnsigned, lhs, rhs) case Long_unsigned_% => expandBinaryOp(LongImpl.remainderUnsigned, lhs, rhs) 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/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 4bbe92a6b0..f14ffcd199 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,8 +70,8 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 147727, - expectedFullLinkSizeWithoutClosure = 86377, + expectedFastLinkSize = 147744, + expectedFullLinkSizeWithoutClosure = 87106, expectedFullLinkSizeWithClosure = 21197, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/project/Build.scala b/project/Build.scala index 3931ed06f3..c69bd57c57 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2060,8 +2060,8 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 424000 to 425000, - fullLink = 281000 to 282000, + fastLink = 425000 to 426000, + fullLink = 282000 to 283000, fastLinkGz = 60000 to 61000, fullLinkGz = 43000 to 44000, )) @@ -2077,8 +2077,8 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 299000 to 300000, - fullLink = 257000 to 258000, + fastLink = 300000 to 301000, + fullLink = 258000 to 259000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, )) From 08ce2c69f69050ec12b829f1b536a78206b0d2f8 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Wed, 7 May 2025 19:55:50 +0900 Subject: [PATCH 27/36] Implement ArrayDeque using scala.Array The original implementation of `ju.ArrayDeque` used `js.Array` for its internal data structure. However, when compiling to WebAssembly, it requires JavaScript interop calls are required to access the underlying js.Array, leading to a significant performance overhead. This commit switches the internal data structure of ju.ArrayDeque to use scala.Array instead, for both the WebAssembly and JavaScript backends. Using `scala.Array` in both environments avoids complicating the logic by conditionally using `js.Array` or `scala.Array` based on whether it's Wasm or JS. This change significantly improves ArrayDeque's performance on WebAssembly and results in only minor performance degradation on JavaScript. See the discussion at https://github.com/scala-js/scala-js/pull/5164 --- .../src/main/scala/java/util/ArrayDeque.scala | 61 ++++++------------- 1 file changed, 20 insertions(+), 41 deletions(-) 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] - } } From f414dae55c11e1aa41209076e4cccb467d6d7576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 27 May 2025 10:50:27 +0200 Subject: [PATCH 28/36] Introduce IR UnaryOps for `numberOfLeadingZeros` (`clz`). This particular operation is used by a number of our core numerical algorithms. It makes sense for it to receive a dedicated opcode. The dedicated opcode lets the optimizer reason about its purity. --- .../org/scalajs/nscplugin/GenJSCode.scala | 6 ++- .../src/main/scala/org/scalajs/ir/Trees.scala | 7 ++- .../src/main/scala/java/lang/Integer.scala | 26 +--------- javalib/src/main/scala/java/lang/Long.scala | 8 +-- .../scalajs/linker/runtime/RuntimeLong.scala | 7 +++ .../linker/backend/emitter/CoreJSLib.scala | 40 +++++++++++++++ .../backend/emitter/FunctionEmitter.scala | 9 ++++ .../linker/backend/emitter/LongImpl.scala | 4 +- .../backend/emitter/PolyfillableBuiltin.scala | 2 + .../linker/backend/emitter/VarField.scala | 3 ++ .../backend/wasmemitter/FunctionEmitter.scala | 6 +++ .../backend/wasmemitter/WasmTransients.scala | 28 +++++------ .../scalajs/linker/checker/IRChecker.scala | 6 ++- .../frontend/optimizer/OptimizerCore.scala | 50 ++++++++----------- project/Build.scala | 2 +- 15 files changed, 123 insertions(+), 81 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 2c39124753..490f8d3d9d 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -7426,11 +7426,13 @@ private object GenJSCode { val byClass: Map[ClassName, Map[MethodName, JavalibOpBody]] = Map( jswkn.BoxedIntegerClass.withSuffix("$") -> Map( m("divideUnsigned", List(I, I), I) -> ArgBinaryOp(binop.Int_unsigned_/), - m("remainderUnsigned", 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("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), 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 d0cc772980..14b748cf48 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -517,6 +517,10 @@ object Trees { // 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 + def isClassOp(op: Code): Boolean = op >= Class_name && op <= Class_superClass @@ -538,7 +542,8 @@ object Trees { case IntToShort => ShortType case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | - String_length | Array_length | IdentityHashCode | Float_toBits => + String_length | Array_length | IdentityHashCode | Float_toBits | + Int_clz | Long_clz => IntType case IntToLong | DoubleToLong | Double_toBits => LongType diff --git a/javalib/src/main/scala/java/lang/Integer.scala b/javalib/src/main/scala/java/lang/Integer.scala index 0bf5d3561f..6c9f33ddfd 100644 --- a/javalib/src/main/scala/java/lang/Integer.scala +++ b/javalib/src/main/scala/java/lang/Integer.scala @@ -274,30 +274,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 faa69bc6d9..4fc7a32505 100644 --- a/javalib/src/main/scala/java/lang/Long.scala +++ b/javalib/src/main/scala/java/lang/Long.scala @@ -419,13 +419,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/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 a0148560ed..13d4823861 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 @@ -661,6 +661,13 @@ 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) 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 e1568b7429..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 @@ -192,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") @@ -954,6 +973,27 @@ private[emitter] object CoreJSLib { } ) ::: 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) 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 cce354512e..d8e6b426a8 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 @@ -2531,6 +2531,15 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { 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 BinaryOp(op, lhs, rhs) => 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 f7f5e09f10..2ca4756700 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 @@ -89,6 +89,7 @@ private[linker] object LongImpl { 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 fromDouble = MethodName("fromDouble", List(DoubleRef), RTLongRef) @@ -100,7 +101,8 @@ private[linker] object LongImpl { divide, remainder, divideUnsigned, remainderUnsigned, or, and, xor, shl, shr, sar, equals_, notEquals, lt, le, gt, ge, - toInt, toFloat, toDouble, bitsToDouble, fromInt, fromDouble, fromDoubleBits + toInt, toFloat, toDouble, bitsToDouble, clz, + fromInt, fromDouble, fromDoubleBits ) // Methods used for intrinsics 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/VarField.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala index 98b3171e05..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 @@ -263,6 +263,8 @@ private[emitter] object VarField { final val checkLongDivisor = mk("$checkLongDivisor") + final val longClz = mk("$longClz") + final val longToFloat = mk("$longToFloat") final val doubleToLong = mk("$doubleToLong") @@ -277,6 +279,7 @@ private[emitter] object VarField { // 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/wasmemitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala index 7fbdeef29e..b15f1ced8c 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 @@ -1685,6 +1685,12 @@ private class FunctionEmitter private ( 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 } tree.tpe 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 2ec686a081..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 @@ -75,27 +73,25 @@ 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 I32Ctz = 1 + final val I32Popcnt = 2 - final val I64Clz = 4 - final val I64Ctz = 5 - final val I64Popcnt = 6 + final val I64Ctz = 3 + final val I64Popcnt = 4 - final val F32Abs = 7 + final val F32Abs = 5 - final val F64Abs = 8 - final val F64Ceil = 9 - final val F64Floor = 10 - final val F64Nearest = 11 - final val F64Sqrt = 12 + 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 => + case I32Ctz | I32Popcnt => IntType - case I64Clz | I64Ctz | I64Popcnt => + case I64Ctz | I64Popcnt => LongType case F32Abs => 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 c4e0ff7d68..c6aef8d11e 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 @@ -538,9 +538,11 @@ private final class IRChecker(linkTimeProperties: LinkTimeProperties, ByteType case ShortToInt => ShortType - case IntToLong | IntToDouble | IntToChar | IntToByte | IntToShort | Float_fromBits => + case IntToLong | IntToDouble | IntToChar | IntToByte | IntToShort | + Float_fromBits | Int_clz => IntType - case LongToInt | LongToDouble | LongToFloat | Double_fromBits => + case LongToInt | LongToDouble | LongToFloat | Double_fromBits | + Long_clz => LongType case FloatToDouble | Float_toBits => FloatType 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 2f0011e32e..f30a174597 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 @@ -2841,17 +2841,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 { @@ -2888,14 +2877,6 @@ private[optimizer] abstract class OptimizerCore( // 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 { @@ -3581,6 +3562,9 @@ private[optimizer] abstract class OptimizerCore( expandBinaryOp(LongImpl.bitsToDouble, arg, PreTransTree(Transient(GetFPBitsDataView))) + case Long_clz => + expandUnaryOp(LongImpl.clz, arg, IntType) + case _ => cont(pretrans) } @@ -3924,6 +3908,23 @@ private[optimizer] abstract class OptimizerCore( 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 + } + case _ => default } @@ -6534,14 +6535,12 @@ 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 LongNLZ = IntegerRotateRight + 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 @@ -6620,9 +6619,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 ), @@ -6666,14 +6662,12 @@ private[optimizer] object OptimizerCore { 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 ), 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, diff --git a/project/Build.scala b/project/Build.scala index c69bd57c57..5838fba1de 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2060,7 +2060,7 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 425000 to 426000, + fastLink = 424000 to 425000, fullLink = 282000 to 283000, fastLinkGz = 60000 to 61000, fullLinkGz = 43000 to 44000, From 1830051f20fe731fe1fa7fc23432be26c0286989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 31 May 2025 17:18:45 +0200 Subject: [PATCH 29/36] Add missing cases in `Printers` for `{Int,Long}_clz`. This was forgotten in f414dae55c11e1aa41209076e4cccb467d6d7576. --- ir/shared/src/main/scala/org/scalajs/ir/Printers.scala | 3 +++ ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala | 3 +++ 2 files changed, 6 insertions(+) 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 facd69b122..f2035fd7f8 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -450,6 +450,9 @@ object Printers { case Float_fromBits => p("(", ")") case Double_toBits => p("(", ")") case Double_fromBits => p("(", ")") + + case Int_clz => p("(", ")") + case Long_clz => p("(", ")") } case BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) => 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 d570445fc8..997e396530 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -519,6 +519,9 @@ class PrintersTest { 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))) } @Test def printPseudoUnaryOp(): Unit = { From b52402cedf6a47db754d6630d417d8fe0a67993d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 27 May 2025 15:12:21 +0200 Subject: [PATCH 30/36] Opt: Avoid the `Long` division in `RuntimeLong.toString`. Through clever analysis of approximation errors, it turns out we can rely on a `Double` division by 10^9 rather than a `Long` division. Since there was already a `Double` division (and remainder) at the end of the `Long` division algorithm, the new implementation should be strictly better. We do have a new call to `js.Math.floor`, but that should be efficient, as `floor` is typically a hardware instruction. We also remove the hackish way of reusing `unsignedDivModHelper` to compute `toString()`. According to the `longMicro` benchmark, this change gives a 3x speed improvement to this code path. --- .../scalajs/linker/runtime/RuntimeLong.scala | 133 ++++++++++++------ .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- project/Build.scala | 6 +- .../testsuite/javalib/lang/LongTest.scala | 27 ++++ 4 files changed, 125 insertions(+), 47 deletions(-) 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 13d4823861..4e99607c0e 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 @@ -92,10 +92,6 @@ object RuntimeLong { */ 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 = _ @@ -566,7 +562,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. * @@ -574,13 +571,77 @@ 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 } } @@ -891,7 +952,7 @@ object RuntimeLong { hiReturn = 0 ahi >>> pow } else { - unsignedDivModHelper(alo, ahi, blo, bhi, AskQuotient).asInstanceOf[Int] + unsignedDivModHelper(alo, ahi, blo, bhi, askQuotient = true) } } } @@ -983,22 +1044,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()`. - * - * The value of `ask` may be one of: + /** Helper for `unsigned_/` and `unsigned_%`. * - * - `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) @@ -1045,31 +1103,24 @@ 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 + } } } 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 f14ffcd199..6fc9029bb0 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 = 147744, - expectedFullLinkSizeWithoutClosure = 87106, - expectedFullLinkSizeWithClosure = 21197, + expectedFastLinkSize = 148312, + expectedFullLinkSizeWithoutClosure = 87480, + expectedFullLinkSizeWithClosure = 20659, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index 5838fba1de..c73788331f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2053,14 +2053,14 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 624000 to 625000, + fastLink = 625000 to 626000, fullLink = 94000 to 95000, fastLinkGz = 75000 to 79000, fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 424000 to 425000, + fastLink = 425000 to 426000, fullLink = 282000 to 283000, fastLinkGz = 60000 to 61000, fullLinkGz = 43000 to 44000, @@ -2077,7 +2077,7 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 300000 to 301000, + fastLink = 301000 to 302000, fullLink = 258000 to 259000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, 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 f9cb9c3e26..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 @@ -274,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 = { From 0c68950bd5367b6e74326bd59379676ba2f07422 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 1 Jun 2025 13:48:22 +0200 Subject: [PATCH 31/36] Opt: Recognize non-nullabe RT long as subtype of LongType Discovered while working on #5077. This allows to remove unnecessary unboxes and unlocks more inlining. --- .../org/scalajs/linker/frontend/optimizer/OptimizerCore.scala | 2 ++ 1 file changed, 2 insertions(+) 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 f30a174597..68a5452531 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 From 3a253e332524a007bfff18e62339a56c0d618e40 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 17 Nov 2024 10:40:41 +0100 Subject: [PATCH 32/36] Optimizer: Fail if we cannot inline RuntimeLong If we cannot inline RuntimeLong (the class or its static methods), something is broken and we should fail. --- .../frontend/optimizer/OptimizerCore.scala | 93 +++++++++---------- 1 file changed, 42 insertions(+), 51 deletions(-) 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 68a5452531..f3841530c5 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 @@ -3504,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) } @@ -3516,24 +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 expandUnaryOp(methodName: MethodName, arg: PreTransform, - resultType: Type = rtLongClassType): TailRec[Tree] = { - pretransformApplyStatic(ApplyFlags.empty, LongImpl.RuntimeLongClass, - MethodIdent(methodName), arg :: Nil, resultType, - isStat = false, usePreTransform = true)( - cont) - } - - def expandBinaryOp(methodName: MethodName, lhs: PreTransform, - rhs: PreTransform, - resultType: Type = rtLongClassType): TailRec[Tree] = { - pretransformApplyStatic(ApplyFlags.empty, LongImpl.RuntimeLongClass, - MethodIdent(methodName), lhs :: 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 { @@ -3542,30 +3533,30 @@ private[optimizer] abstract class OptimizerCore( (op: @switch) match { case IntToLong => - expandUnaryOp(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 => - expandUnaryOp(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 => - expandBinaryOp(LongImpl.fromDoubleBits, + expand(LongImpl.fromDoubleBits, arg, PreTransTree(Transient(GetFPBitsDataView))) case Double_fromBits if config.coreSpec.esFeatures.esVersion >= ESVersion.ES2015 => - expandBinaryOp(LongImpl.bitsToDouble, + expand(LongImpl.bitsToDouble, arg, PreTransTree(Transient(GetFPBitsDataView))) case Long_clz => - expandUnaryOp(LongImpl.clz, arg, IntType) + expand(LongImpl.clz, arg) case _ => cont(pretrans) @@ -3575,37 +3566,37 @@ private[optimizer] abstract class OptimizerCore( import BinaryOp._ (op: @switch) match { - case Long_+ => expandBinaryOp(LongImpl.add, lhs, rhs) + case Long_+ => expand(LongImpl.add, lhs, rhs) case Long_- => lhs match { case PreTransLit(LongLiteral(0L)) => - expandUnaryOp(LongImpl.neg, rhs) + expand(LongImpl.neg, rhs) case _ => - expandBinaryOp(LongImpl.sub, lhs, rhs) + expand(LongImpl.sub, lhs, rhs) } - case Long_* => expandBinaryOp(LongImpl.mul, lhs, rhs) - case Long_/ => expandBinaryOp(LongImpl.divide, lhs, rhs) - case Long_% => expandBinaryOp(LongImpl.remainder, 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_& => expandBinaryOp(LongImpl.and, lhs, rhs) - case Long_| => expandBinaryOp(LongImpl.or, lhs, rhs) - case Long_^ => expandBinaryOp(LongImpl.xor, 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_<< => expandBinaryOp(LongImpl.shl, lhs, rhs) - case Long_>>> => expandBinaryOp(LongImpl.shr, lhs, rhs) - case Long_>> => expandBinaryOp(LongImpl.sar, 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_== => expandBinaryOp(LongImpl.equals_, lhs, rhs) - case Long_!= => expandBinaryOp(LongImpl.notEquals, lhs, rhs) - case Long_< => expandBinaryOp(LongImpl.lt, lhs, rhs) - case Long_<= => expandBinaryOp(LongImpl.le, lhs, rhs) - case Long_> => expandBinaryOp(LongImpl.gt, lhs, rhs) - case Long_>= => expandBinaryOp(LongImpl.ge, 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_/ => expandBinaryOp(LongImpl.divideUnsigned, lhs, rhs) - case Long_unsigned_% => expandBinaryOp(LongImpl.remainderUnsigned, lhs, rhs) + case Long_unsigned_/ => expand(LongImpl.divideUnsigned, lhs, rhs) + case Long_unsigned_% => expand(LongImpl.remainderUnsigned, lhs, rhs) case _ => cont(pretrans) From 83cdafc23d43313fa16fcdeb5b0bedee38347e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 29 May 2025 15:06:12 +0200 Subject: [PATCH 33/36] Opt: Branchless addition, subtraction and negation for `RuntimeLong`. Hacker's Delight offers branchless formulas for double-word additions and subtraction, without access to the machine's carry bit. Although the new formula contains more elementary instructions, removal of the branch is significant. When one operand is constant, folding reduces to 3 bitwise operations to compute the carry, which is very fast. When both operands are variable, then the carry is often 50/50 unpredictable, which means the branch is unpredictable, and removing it is worth the full 5 bitwise operations anyway. Negation does not need a special code path anymore. Regular folding of the `0L - b` formula yields optimal, branchless code anyway. We remove `RuntimeLong.neg` and its code paths in the optimizer and emitter. While we're there, we also remove `RuntimeLong.not`, since the regular code paths for `-1L ^ b` fold in the same way. This change reduces execution time of the `sha512` benchmark by a whopping 25-30%. --- .../scalajs/linker/runtime/RuntimeLong.scala | 64 ++++---- .../backend/emitter/FunctionEmitter.scala | 42 ++--- .../linker/backend/emitter/LongImpl.scala | 4 - .../frontend/optimizer/OptimizerCore.scala | 146 +++++++++++++++--- 4 files changed, 179 insertions(+), 77 deletions(-) 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 4e99607c0e..f6d30c1dd7 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 @@ -158,10 +158,6 @@ object RuntimeLong { // Bitwise operations - @inline - def not(a: RuntimeLong): RuntimeLong = - new RuntimeLong(~a.lo, ~a.hi) - @inline def or(a: RuntimeLong, b: RuntimeLong): RuntimeLong = new RuntimeLong(a.lo | b.lo, a.hi | b.hi) @@ -272,31 +268,31 @@ object RuntimeLong { // Arithmetic operations - @inline - def neg(a: RuntimeLong): RuntimeLong = { - val lo = a.lo - val hi = a.hi - new RuntimeLong(inline_lo_unary_-(lo), inline_hi_unary_-(lo, hi)) - } - @inline 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 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 @@ -548,7 +544,8 @@ object RuntimeLong { 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) } @@ -656,8 +653,8 @@ object RuntimeLong { 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) } @@ -900,7 +897,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) } } @@ -993,7 +990,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 } } @@ -1124,12 +1121,6 @@ object RuntimeLong { } } - @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 @@ -1222,17 +1213,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/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 d8e6b426a8..604aa09971 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 @@ -2679,17 +2679,20 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { else 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 - genLongApplyStatic(LongImpl.neg, newRhs) - case _ => - if (useBigIntForLongs) + case _ => wrapBigInt64(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs)) - else - genLongApplyStatic(LongImpl.sub, newLhs, 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) @@ -2730,17 +2733,20 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { else 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 - genLongApplyStatic(LongImpl.not, newRhs) - case _ => - if (useBigIntForLongs) + case _ => wrapBigInt64(js.BinaryOp(JSBinaryOp.^, newLhs, newRhs)) - else - genLongApplyStatic(LongImpl.xor, newLhs, 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) 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 2ca4756700..10d0acf68b 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 @@ -58,9 +58,6 @@ private[linker] object LongImpl { // Operator methods - final val neg = unaryOp("neg") - final val not = unaryOp("not") - final val add = binaryOp("add") final val sub = binaryOp("sub") final val mul = binaryOp("mul") @@ -96,7 +93,6 @@ private[linker] object LongImpl { final val fromDoubleBits = MethodName("fromDoubleBits", List(DoubleRef, ObjectRef), RTLongRef) val OperatorMethods = Set( - neg, not, add, sub, mul, divide, remainder, divideUnsigned, remainderUnsigned, or, and, xor, shl, shr, sar, 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 f3841530c5..08dbf1d1cf 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 @@ -3567,15 +3567,7 @@ private[optimizer] abstract class OptimizerCore( (op: @switch) match { case Long_+ => expand(LongImpl.add, lhs, rhs) - - case Long_- => - lhs match { - case PreTransLit(LongLiteral(0L)) => - expand(LongImpl.neg, rhs) - case _ => - expand(LongImpl.sub, 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) @@ -4281,6 +4273,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 } @@ -4299,6 +4305,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 } @@ -4335,10 +4348,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 } @@ -4357,7 +4375,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), @@ -4369,10 +4387,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 } @@ -4388,7 +4411,7 @@ 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 => @@ -4404,10 +4427,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 } @@ -5057,6 +5085,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 = { From a366057b1a2ba4dc69cade109522cbab26799d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 30 May 2025 18:19:17 +0200 Subject: [PATCH 34/36] Introduce IR ops for unsigned extension and comparisons. This completes the set of `UnaryOp`s and `BinaryOp`s to directly manipulate the unsigned representation of integers. Unlike other operations, such as `Int_unsigned_/`, the unsigned extension and comparisons have efficient (and convenient) implementations in user land. It is common for regular code to directly use the efficient implementation (e.g., `x.toLong & 0xffffffffL`) instead of the dedicated library method (`Integer.toUnsignedLong`). If we only replaced the body of the library methods with IR nodes, we would miss improvements in all the other code. Therefore, in this case, we instead recognize the user-space patterns in the optimizer, and replace them with the unsigned IR operations through folding. Moreover, for unsigned comparisons, we also recognize the patterns in the compiler backend. The purpose here is mostly to make sure that all these opcodes end up in the serialized IR, so that we effectively test them along the entire pipeline. When targeting JavaScript, the new IR nodes do not actually make any difference. For `int` operations, the Emitter sort of "undoes" the folding of the optimizer to implement them. That said, it could choose an alternative implementation based on `>>> 0`, which we should investigate in the future. For `Long`s, the subexpressions of the patterns are expanded into the `RuntimeLong` operations before folding gets a chance to recognize them (when they have not been transformed by the compiler backend). That's fine, because internal folding of the underlying `int` operations will do the best possible thing anyway. The size increase is only due to the additional always-reachable methods in `RuntimeLong`. Those can be removed by standard JS minifiers. When targeting Wasm, this allows the emitter to produce the dedicated Wasm opcodes, which are more likely to be efficient. To be fair, we could have achieved the same result by recognizing the patterns in the Wasm emitter instead. The deeper reason to add those IR operations is for completeness. They were the last operations from a standard set that were missing in the IR. --- .../org/scalajs/nscplugin/GenJSCode.scala | 127 ++++++++---- .../nscplugin/test/OptimizationTest.scala | 66 ++++++ .../main/scala/org/scalajs/ir/Printers.scala | 12 ++ .../src/main/scala/org/scalajs/ir/Trees.scala | 18 +- .../scala/org/scalajs/ir/PrintersTest.scala | 20 ++ .../src/main/scala/java/lang/Integer.scala | 14 +- javalib/src/main/scala/java/lang/Long.scala | 11 +- .../scalajs/linker/runtime/RuntimeLong.scala | 52 +++++ .../backend/emitter/FunctionEmitter.scala | 38 ++++ .../linker/backend/emitter/LongImpl.scala | 9 +- .../backend/wasmemitter/FunctionEmitter.scala | 13 ++ .../scalajs/linker/checker/IRChecker.scala | 8 +- .../frontend/optimizer/OptimizerCore.scala | 189 +++++++++++++----- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- project/Build.scala | 14 +- 15 files changed, 478 insertions(+), 119 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 490f8d3d9d..3839c61e8c 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -4526,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 = @@ -7357,6 +7385,28 @@ private object GenJSCode { } } + 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 @@ -7425,6 +7475,7 @@ private object GenJSCode { 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) 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/Printers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala index f2035fd7f8..216bec733e 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -453,6 +453,8 @@ object Printers { case Int_clz => p("(", ")") case Long_clz => p("(", ")") + + case UnsignedIntToLong => p("(", ")") } case BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) => @@ -584,6 +586,16 @@ object Printers { 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/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index 14b748cf48..ca2c76dcc8 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -520,6 +520,7 @@ object Trees { // 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 @@ -545,7 +546,7 @@ object Trees { String_length | Array_length | IdentityHashCode | Float_toBits | Int_clz | Long_clz => IntType - case IntToLong | DoubleToLong | Double_toBits => + case IntToLong | DoubleToLong | Double_toBits | UnsignedIntToLong => LongType case DoubleToFloat | LongToFloat | Float_fromBits => FloatType @@ -685,11 +686,22 @@ object Trees { 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 @@ -699,7 +711,9 @@ 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 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 997e396530..ea68b14864 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -522,6 +522,8 @@ class PrintersTest { 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 = { @@ -683,6 +685,24 @@ class PrintersTest { 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/Integer.scala b/javalib/src/main/scala/java/lang/Integer.scala index 6c9f33ddfd..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 = { diff --git a/javalib/src/main/scala/java/lang/Long.scala b/javalib/src/main/scala/java/lang/Long.scala index 4fc7a32505..de3ed8ae94 100644 --- a/javalib/src/main/scala/java/lang/Long.scala +++ b/javalib/src/main/scala/java/lang/Long.scala @@ -337,16 +337,19 @@ 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) + // 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 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 f6d30c1dd7..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 @@ -156,6 +156,50 @@ object RuntimeLong { else ahi > bhi } + @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 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 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 @@ -730,6 +774,10 @@ object RuntimeLong { 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) @@ -1204,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) 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 604aa09971..f7074cb469 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 @@ -2540,6 +2540,12 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { 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) => @@ -2552,6 +2558,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case _ => genGetDataOf(jsTree) } + def flipSign(arg: js.Tree): js.Tree = arg match { + case js.IntLiteral(value) => js.IntLiteral(Int.MinValue ^ value) + case _ => js.IntLiteral(Int.MinValue) ^ arg + } + (op: @switch) match { case === | !== => /* Semantically, this is an `Object.is` test in JS. However, we @@ -2843,6 +2854,33 @@ 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) + + // TODO Investigate whether using `>>> 0` would produce better code or not + case Int_unsigned_< => js.BinaryOp(JSBinaryOp.<, flipSign(newLhs), flipSign(newRhs)) + case Int_unsigned_<= => js.BinaryOp(JSBinaryOp.<=, flipSign(newLhs), flipSign(newRhs)) + case Int_unsigned_> => js.BinaryOp(JSBinaryOp.>, flipSign(newLhs), flipSign(newRhs)) + case Int_unsigned_>= => js.BinaryOp(JSBinaryOp.>=, flipSign(newLhs), flipSign(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) => 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 10d0acf68b..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 @@ -81,6 +81,10 @@ private[linker] object LongImpl { 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) @@ -89,6 +93,7 @@ private[linker] object LongImpl { 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) @@ -96,9 +101,9 @@ private[linker] object LongImpl { add, sub, mul, divide, remainder, divideUnsigned, remainderUnsigned, or, and, xor, shl, shr, sar, - equals_, notEquals, lt, le, gt, ge, + equals_, notEquals, lt, le, gt, ge, ltu, leu, gtu, geu, toInt, toFloat, toDouble, bitsToDouble, clz, - fromInt, fromDouble, fromDoubleBits + fromInt, fromUnsignedInt, fromDouble, fromDoubleBits ) // Methods used for intrinsics 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 b15f1ced8c..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 @@ -1691,6 +1691,9 @@ private class FunctionEmitter private ( case Long_clz => fb += wa.I64Clz fb += wa.I32WrapI64 + + case UnsignedIntToLong => + fb += wa.I64ExtendI32U } tree.tpe @@ -1974,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 } } 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 c6aef8d11e..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 @@ -539,7 +539,7 @@ private final class IRChecker(linkTimeProperties: LinkTimeProperties, case ShortToInt => ShortType case IntToLong | IntToDouble | IntToChar | IntToByte | IntToShort | - Float_fromBits | Int_clz => + Float_fromBits | Int_clz | UnsignedIntToLong => IntType case LongToInt | LongToDouble | LongToFloat | Double_fromBits | Long_clz => @@ -574,12 +574,14 @@ private final class IRChecker(linkTimeProperties: LinkTimeProperties, case 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_<= | 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_% => + Long_unsigned_/ | Long_unsigned_% | + Long_unsigned_< | Long_unsigned_<= | Long_unsigned_> | Long_unsigned_>= => LongType case Float_+ | Float_- | Float_* | Float_/ | Float_% => FloatType 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 08dbf1d1cf..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 @@ -3558,6 +3558,9 @@ private[optimizer] abstract class OptimizerCore( case Long_clz => expand(LongImpl.clz, arg) + case UnsignedIntToLong => + expand(LongImpl.fromUnsignedInt, arg) + case _ => cont(pretrans) } @@ -3590,6 +3593,11 @@ private[optimizer] abstract class OptimizerCore( 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) } @@ -3625,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_>= @@ -3632,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_== @@ -3910,6 +3928,16 @@ private[optimizer] abstract class OptimizerCore( default } + // Unsigned int to long + + case UnsignedIntToLong => + arg match { + case PreTransLit(IntLiteral(v)) => + PreTransLit(LongLiteral(Integer.toUnsignedLong(v))) + case _ => + default + } + case _ => default } @@ -4465,54 +4493,72 @@ private[optimizer] abstract class OptimizerCore( 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(op == Int_<= || op == Int_>=) + booleanLit(signedOp == Int_<= || signedOp == Int_>=) case (PreTransLit(IntLiteral(_)), _) => foldBinaryOp(flippedOp, rhs, lhs) @@ -4679,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) @@ -4765,49 +4814,60 @@ private[optimizer] abstract class OptimizerCore( 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_<= @@ -4821,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))) @@ -4833,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) { @@ -4861,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 @@ -4884,7 +4947,7 @@ private[optimizer] abstract class OptimizerCore( }.toPreTransform case (PreTransLocalDef(l), PreTransLocalDef(r)) if l eq r => - booleanLit(op == Long_<= || op == Long_>=) + booleanLit(signedOp == Long_<= || signedOp == Long_>=) case (PreTransLit(LongLiteral(_)), _) => foldBinaryOp(flippedOp, rhs, lhs) @@ -6540,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) 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 6fc9029bb0..1d9a64b96c 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 = 148312, - expectedFullLinkSizeWithoutClosure = 87480, - expectedFullLinkSizeWithClosure = 20659, + expectedFastLinkSize = 149300, + expectedFullLinkSizeWithoutClosure = 88451, + expectedFullLinkSizeWithClosure = 20704, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index c73788331f..49ee4552a5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2053,16 +2053,16 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 625000 to 626000, + fastLink = 626000 to 627000, fullLink = 94000 to 95000, fastLinkGz = 75000 to 79000, fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 425000 to 426000, - fullLink = 282000 to 283000, - fastLinkGz = 60000 to 61000, + fastLink = 426000 to 427000, + fullLink = 283000 to 284000, + fastLinkGz = 61000 to 62000, fullLinkGz = 43000 to 44000, )) } @@ -2070,15 +2070,15 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 442000 to 443000, + fastLink = 443000 to 444000, fullLink = 90000 to 91000, fastLinkGz = 57000 to 58000, fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 301000 to 302000, - fullLink = 258000 to 259000, + fastLink = 302000 to 303000, + fullLink = 259000 to 260000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, )) From 6d179c978ddce2f71fa9ab05b7a1d0f53017eddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 4 Jun 2025 17:11:18 +0200 Subject: [PATCH 35/36] Use `x >>> 0` instead of `x ^ 0x80000000` for unsigned comparisons. Benchmarks show that this is slightly faster. Inspection of the source code of v8 also suggests that they do not recognize `x ^ 0x80000000` as doing anything special, but they do recognize `x >>> 0` as emitting an "`Unsigned32`", and they do generate unsigned comparisons when both inputs are known to be `Unsigned32`. --- .../closure/ClosureAstTransformer.scala | 2 ++ .../backend/emitter/FunctionEmitter.scala | 22 +++++++++---------- .../linker/backend/javascript/Printers.scala | 6 ++++- .../linker/backend/javascript/Trees.scala | 3 +++ .../org/scalajs/linker/LibrarySizeTest.scala | 4 ++-- project/Build.scala | 6 ++--- 6 files changed, 25 insertions(+), 18 deletions(-) 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/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 f7074cb469..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 @@ -2208,8 +2208,12 @@ 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 = - 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 { @@ -2558,11 +2562,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case _ => genGetDataOf(jsTree) } - def flipSign(arg: js.Tree): js.Tree = arg match { - case js.IntLiteral(value) => js.IntLiteral(Int.MinValue ^ value) - case _ => js.IntLiteral(Int.MinValue) ^ arg - } - (op: @switch) match { case === | !== => /* Semantically, this is an `Object.is` test in JS. However, we @@ -2855,11 +2854,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Class_newArray => js.Apply(extractClassData(lhs, newLhs) DOT cpn.newArray, newRhs :: Nil) - // TODO Investigate whether using `>>> 0` would produce better code or not - case Int_unsigned_< => js.BinaryOp(JSBinaryOp.<, flipSign(newLhs), flipSign(newRhs)) - case Int_unsigned_<= => js.BinaryOp(JSBinaryOp.<=, flipSign(newLhs), flipSign(newRhs)) - case Int_unsigned_> => js.BinaryOp(JSBinaryOp.>, flipSign(newLhs), flipSign(newRhs)) - case Int_unsigned_>= => js.BinaryOp(JSBinaryOp.>=, flipSign(newLhs), flipSign(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 Int_unsigned_>= => js.BinaryOp(JSBinaryOp.>=, shr0(newLhs), shr0(newRhs)) case Long_unsigned_< => if (useBigIntForLongs) 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/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 1d9a64b96c..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,8 +70,8 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 149300, - expectedFullLinkSizeWithoutClosure = 88451, + expectedFastLinkSize = 148960, + expectedFullLinkSizeWithoutClosure = 88111, expectedFullLinkSizeWithClosure = 20704, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/project/Build.scala b/project/Build.scala index 49ee4552a5..2141d5f3b3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2053,14 +2053,14 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 626000 to 627000, + fastLink = 625000 to 626000, fullLink = 94000 to 95000, fastLinkGz = 75000 to 79000, fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 426000 to 427000, + fastLink = 425000 to 426000, fullLink = 283000 to 284000, fastLinkGz = 61000 to 62000, fullLinkGz = 43000 to 44000, @@ -2077,7 +2077,7 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 302000 to 303000, + fastLink = 301000 to 302000, fullLink = 259000 to 260000, fastLinkGz = 47000 to 48000, fullLinkGz = 42000 to 43000, From 2539e59b9d5f4bf84a3f67042b44494085298b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 6 Jun 2025 11:04:17 +0200 Subject: [PATCH 36/36] Opt: Generalize the `RuntimeLong.toString` optimization to any base. With some more care to the choice of divisors (`radixPowLength`) we use in `jl.Long.to{,Unsigned}String`, we can generalize the optimization of `RuntimeLong.toString` to all the possible bases. --- javalib/src/main/scala/java/lang/Long.scala | 108 +++++++++++++------- 1 file changed, 71 insertions(+), 37 deletions(-) diff --git a/javalib/src/main/scala/java/lang/Long.scala b/javalib/src/main/scala/java/lang/Long.scala index 4fc7a32505..f343f69344 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)