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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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]