From 797953158c3f7ec6130832c71086c2722b2bbc70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 22 Jan 2025 11:16:06 +0100 Subject: [PATCH 01/30] Towards 1.18.3. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- project/Build.scala | 2 +- 2 files 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 ca9c29094f..0f03e6a638 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.18.2", + current = "1.18.3-SNAPSHOT", binaryEmitted = "1.18" ) diff --git a/project/Build.scala b/project/Build.scala index 3e2f1c72f4..8749ef5c74 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -395,7 +395,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.1", "1.18.2") val previousVersion = previousVersions.last val previousBinaryCrossVersion = CrossVersion.binaryWith("sjs1_", "") From 795d3e062de91e341b072e5b25e61e4faa1e07aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 31 Jan 2025 11:06:31 +0100 Subject: [PATCH 02/30] Bump the Scala 3 version used for source compat of `ir/` to 3.6.3. `private[this]` is deprecated in favor of `private`. This should not have a real impact on post-JIT performance. `var x = _` is deprecated in favor `= scala.compiletime.unitialized`. The latter does not exist in Scala 2, so we use `= null`. We only used that during the construction of a `Deserializer` instance. It could theoretically affect performance, but not in any measurable way compared to everything else that happens in a `Deserializer`. --- Jenkinsfile | 2 +- .../main/scala/org/scalajs/ir/Hashers.scala | 4 +- .../scala/org/scalajs/ir/Serializers.scala | 54 +++++++++---------- project/Build.scala | 2 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index aa8a89111d..8f6ecc3c8e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -575,7 +575,7 @@ def otherScalaVersions = [ "2.12.15" ] -def scala3Version = "3.3.4" +def scala3Version = "3.6.3" def allESVersions = [ "ES5_1", 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 4f6c3eeb72..d37b6ffdfc 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -136,9 +136,9 @@ object Hashers { } private final class TreeHasher { - private[this] val digestBuilder = new SHA1.DigestBuilder + private val digestBuilder = new SHA1.DigestBuilder - private[this] val digestStream = { + private val digestStream = { new DataOutputStream(new OutputStream { def write(b: Int): Unit = digestBuilder.update(b.toByte) 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 707dfc61d7..4866e36457 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -121,24 +121,24 @@ object Serializers { } private final class Serializer { - private[this] val bufferUnderlying = new JumpBackByteArrayOutputStream - private[this] val buffer = new DataOutputStream(bufferUnderlying) + private val bufferUnderlying = new JumpBackByteArrayOutputStream + private val buffer = new DataOutputStream(bufferUnderlying) - private[this] val files = mutable.ListBuffer.empty[URI] - private[this] val fileIndexMap = mutable.Map.empty[URI, Int] + private val files = mutable.ListBuffer.empty[URI] + private val fileIndexMap = mutable.Map.empty[URI, Int] private def fileToIndex(file: URI): Int = fileIndexMap.getOrElseUpdate(file, (files += file).size - 1) - private[this] val encodedNames = mutable.ListBuffer.empty[UTF8String] - private[this] val encodedNameIndexMap = mutable.Map.empty[EncodedNameKey, Int] + private val encodedNames = mutable.ListBuffer.empty[UTF8String] + private val encodedNameIndexMap = mutable.Map.empty[EncodedNameKey, Int] private def encodedNameToIndex(encoded: UTF8String): Int = { val byteString = new EncodedNameKey(encoded) encodedNameIndexMap.getOrElseUpdate(byteString, (encodedNames += encoded).size - 1) } - private[this] val methodNames = mutable.ListBuffer.empty[MethodName] - private[this] val methodNameIndexMap = mutable.Map.empty[MethodName, Int] + private val methodNames = mutable.ListBuffer.empty[MethodName] + private val methodNameIndexMap = mutable.Map.empty[MethodName, Int] private def methodNameToIndex(methodName: MethodName): Int = { methodNameIndexMap.getOrElseUpdate(methodName, { // need to reserve the internal simple names @@ -159,12 +159,12 @@ object Serializers { }) } - private[this] val strings = mutable.ListBuffer.empty[String] - private[this] val stringIndexMap = mutable.Map.empty[String, Int] + private val strings = mutable.ListBuffer.empty[String] + private val stringIndexMap = mutable.Map.empty[String, Int] private def stringToIndex(str: String): Int = stringIndexMap.getOrElseUpdate(str, (strings += str).size - 1) - private[this] var lastPosition: Position = Position.NoPosition + private var lastPosition: Position = Position.NoPosition def serialize(stream: OutputStream, classDef: ClassDef): Unit = { // Write tree to buffer and record files, names and strings @@ -988,16 +988,16 @@ object Serializers { private final class Deserializer(buf: ByteBuffer) { require(buf.order() == ByteOrder.BIG_ENDIAN) - private[this] var hacks: Hacks = _ - private[this] var files: Array[URI] = _ - private[this] var encodedNames: Array[UTF8String] = _ - private[this] var localNames: Array[LocalName] = _ - private[this] var labelNames: Array[LabelName] = _ - private[this] var simpleFieldNames: Array[SimpleFieldName] = _ - private[this] var simpleMethodNames: Array[SimpleMethodName] = _ - private[this] var classNames: Array[ClassName] = _ - private[this] var methodNames: Array[MethodName] = _ - private[this] var strings: Array[String] = _ + private var hacks: Hacks = null + private var files: Array[URI] = null + private var encodedNames: Array[UTF8String] = null + private var localNames: Array[LocalName] = null + private var labelNames: Array[LabelName] = null + private var simpleFieldNames: Array[SimpleFieldName] = null + private var simpleMethodNames: Array[SimpleMethodName] = null + private var classNames: Array[ClassName] = null + private var methodNames: Array[MethodName] = null + private var strings: Array[String] = null /** Uniqueness cache for FieldName's. * @@ -1008,13 +1008,13 @@ object Serializers { * to make them all `eq`, consuming less memory and speeding up equality * tests. */ - private[this] val uniqueFieldNames = mutable.AnyRefMap.empty[FieldName, FieldName] + private val uniqueFieldNames = mutable.AnyRefMap.empty[FieldName, FieldName] - private[this] var lastPosition: Position = Position.NoPosition + private var lastPosition: Position = Position.NoPosition - private[this] var enclosingClassName: ClassName = _ - private[this] var thisTypeForHack: Option[Type] = None - private[this] var patchDynamicImportThunkSuperCtorCall: Boolean = false + private var enclosingClassName: ClassName = null + private var thisTypeForHack: Option[Type] = None + private var patchDynamicImportThunkSuperCtorCall: Boolean = false def deserializeEntryPointsInfo(): EntryPointsInfo = { hacks = new Hacks(sourceVersion = readHeader()) @@ -2535,7 +2535,7 @@ object Serializers { } private class OptionBuilder[T] { - private[this] var value: Option[T] = None + private var value: Option[T] = None def +=(x: T): Unit = { require(value.isEmpty) diff --git a/project/Build.scala b/project/Build.scala index 8749ef5c74..446aeebfab 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1061,7 +1061,7 @@ object Build { */ scalacOptions ++= { if (scalaVersion.value.startsWith("3.")) - List("-Ysafe-init") + List("-Wsafe-init") else Nil }, From 2a77c9cea28dfa4086802f230035366e55c20fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 22 Jan 2025 12:11:22 +0100 Subject: [PATCH 03/30] Refactoring: Generic notion of CheckingPhase for the IR checkers. We previously had two boolean options for deciding what IR is allowed or not between phases. Of the four possible combinations of values, one was actually invalid. With the introduction of the desugarer, there would have been *three* such boolean options. Only 4 out of their 8 combinations would have been valid. We now introduce a generic concept of `CheckingPhase`. Some IR features are allowed as output of each `CheckingPhase`. Since the `ClassDefChecker` and `IRChecker` must agree on what features are valid when, we factor that logic into `FeatureSet`. It represents a set of logical features, and can compute which set is valid for each phase. --- .../scalajs/linker/analyzer/Analyzer.scala | 14 ++-- .../scalajs/linker/analyzer/InfoLoader.scala | 36 +++------ .../linker/checker/CheckingPhase.scala | 28 +++++++ .../linker/checker/ClassDefChecker.scala | 39 +++++----- .../scalajs/linker/checker/FeatureSet.scala | 76 +++++++++++++++++++ .../scalajs/linker/checker/IRChecker.scala | 19 +++-- .../scalajs/linker/frontend/BaseLinker.scala | 8 +- .../org/scalajs/linker/frontend/Refiner.scala | 10 ++- .../org/scalajs/linker/BaseLinkerTest.scala | 5 +- .../linker/checker/ClassDefCheckerTest.scala | 8 +- .../linker/testutils/LinkingUtils.scala | 4 +- 11 files changed, 173 insertions(+), 74 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/checker/FeatureSet.scala 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 dc4dab1816..9dc14c1574 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 @@ -29,6 +29,7 @@ import org.scalajs.ir.Trees.{MemberNamespace, JSNativeLoadSpec} import org.scalajs.ir.Types.ClassRef import org.scalajs.linker._ +import org.scalajs.linker.checker.CheckingPhase import org.scalajs.linker.frontend.IRLoader import org.scalajs.linker.interface._ import org.scalajs.linker.interface.unstable.ModuleInitializerImpl @@ -43,15 +44,10 @@ import Analysis._ import Infos.{NamespacedMethodName, ReachabilityInfo, ReachabilityInfoInClass} final class Analyzer(config: CommonPhaseConfig, initial: Boolean, - checkIR: Boolean, failOnError: Boolean, irLoader: IRLoader) { - - private val infoLoader: InfoLoader = { - new InfoLoader(irLoader, - if (!checkIR) InfoLoader.NoIRCheck - else if (initial) InfoLoader.InitialIRCheck - else InfoLoader.InternalIRCheck - ) - } + checkIRFor: Option[CheckingPhase], failOnError: Boolean, irLoader: IRLoader) { + + private val infoLoader: InfoLoader = + new InfoLoader(irLoader, checkIRFor) 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 7eeb5d197b..83003e6be5 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 @@ -22,14 +22,14 @@ import org.scalajs.ir.Trees._ import org.scalajs.logging._ -import org.scalajs.linker.checker.ClassDefChecker +import org.scalajs.linker.checker._ import org.scalajs.linker.frontend.IRLoader import org.scalajs.linker.interface.LinkingException import org.scalajs.linker.CollectionsCompat.MutableMapCompatOps import Platform.emptyThreadSafeMap -private[analyzer] final class InfoLoader(irLoader: IRLoader, irCheckMode: InfoLoader.IRCheckMode) { +private[analyzer] final class InfoLoader(irLoader: IRLoader, checkIRFor: Option[CheckingPhase]) { private var logger: Logger = _ private val cache = emptyThreadSafeMap[ClassName, InfoLoader.ClassInfoCache] @@ -44,7 +44,7 @@ private[analyzer] final class InfoLoader(irLoader: IRLoader, irCheckMode: InfoLo implicit ec: ExecutionContext): Option[Future[Infos.ClassInfo]] = { if (irLoader.classExists(className)) { val infoCache = cache.getOrElseUpdate(className, - new InfoLoader.ClassInfoCache(className, irLoader, irCheckMode)) + new InfoLoader.ClassInfoCache(className, irLoader, checkIRFor)) Some(infoCache.loadInfo(logger)) } else { None @@ -58,15 +58,9 @@ private[analyzer] final class InfoLoader(irLoader: IRLoader, irCheckMode: InfoLo } private[analyzer] object InfoLoader { - sealed trait IRCheckMode - - case object NoIRCheck extends IRCheckMode - case object InitialIRCheck extends IRCheckMode - case object InternalIRCheck extends IRCheckMode - private type MethodInfos = Array[Map[MethodName, Infos.MethodInfo]] - private class ClassInfoCache(className: ClassName, irLoader: IRLoader, irCheckMode: InfoLoader.IRCheckMode) { + private class ClassInfoCache(className: ClassName, irLoader: IRLoader, checkIRFor: Option[CheckingPhase]) { private var cacheUsed: Boolean = false private var version: Version = Version.Unversioned private var info: Future[Infos.ClassInfo] = _ @@ -86,26 +80,18 @@ private[analyzer] object InfoLoader { if (!version.sameVersion(newVersion)) { version = newVersion info = irLoader.loadClassDef(className).map { tree => - irCheckMode match { - case InfoLoader.NoIRCheck => - // no check - - case InfoLoader.InitialIRCheck => - val errorCount = ClassDefChecker.check(tree, - postBaseLinker = false, postOptimizer = false, logger) - if (errorCount != 0) { + for (previousPhase <- checkIRFor) { + val errorCount = ClassDefChecker.check(tree, previousPhase, logger) + if (errorCount != 0) { + if (previousPhase == CheckingPhase.Compiler) { throw new LinkingException( s"There were $errorCount ClassDef checking errors.") - } - - case InfoLoader.InternalIRCheck => - val errorCount = ClassDefChecker.check(tree, - postBaseLinker = true, postOptimizer = true, logger) - if (errorCount != 0) { + } else { throw new LinkingException( - s"There were $errorCount ClassDef checking errors after optimizing. " + + s"There were $errorCount ClassDef checking errors after transformations. " + "Please report this as a bug.") } + } } generateInfos(tree) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala new file mode 100644 index 0000000000..ff198744de --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala @@ -0,0 +1,28 @@ +/* + * 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.checker + +/** A phase *after which* we are checking IR. + * + * When checking IR (with `ClassDefChecker` or `IRChecker`), different nodes + * and transients are allowed between different phases. The `CheckingPhase` + * records the *previous* phase to run before the check. We are therefore + * checking that the IR is a valid *output* of the target phase. + */ +sealed abstract class CheckingPhase + +object CheckingPhase { + case object Compiler extends CheckingPhase + case object BaseLinker extends CheckingPhase + case object Optimizer extends CheckingPhase +} 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 103d19f963..b3dd10acb8 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 @@ -28,11 +28,13 @@ import org.scalajs.linker.standard.LinkedClass /** Checker for the validity of the IR. */ private final class ClassDefChecker(classDef: ClassDef, - postBaseLinker: Boolean, postOptimizer: Boolean, reporter: ErrorReporter) { + previousPhase: CheckingPhase, reporter: ErrorReporter) { import ClassDefChecker._ import reporter.reportError + private val featureSet = FeatureSet.allowedAfter(previousPhase) + private[this] val isJLObject = classDef.name.name == ObjectClass private[this] val instanceThisType: Type = { @@ -107,7 +109,7 @@ private final class ClassDefChecker(classDef: ClassDef, * module classes and JS classes that are never instantiated. The classes * may still exist because their class data are accessed. */ - if (!postBaseLinker) { + if (!featureSet.supports(FeatureSet.OptionalConstructors)) { /* Check that we have exactly 1 constructor in a module class. This goes * together with `checkMethodDef`, which checks that a constructor in a * module class must be 0-arg. @@ -489,7 +491,7 @@ private final class ClassDefChecker(classDef: ClassDef, private def checkMethodNameNamespace(name: MethodName, namespace: MemberNamespace)( implicit ctx: ErrorContext): Unit = { if (name.isReflectiveProxy) { - if (postBaseLinker) { + if (featureSet.supports(FeatureSet.ReflectiveProxies)) { if (namespace != MemberNamespace.Public) reportError("reflective profixes are only allowed in the public namespace") } else { @@ -557,7 +559,7 @@ private final class ClassDefChecker(classDef: ClassDef, } if (rest.isEmpty) { - if (!postOptimizer) + if (!featureSet.supports(FeatureSet.RelaxedCtorBodies)) reportError(i"Constructor must contain a delegate constructor call") val bodyStatsStoreModulesHandled = @@ -576,7 +578,7 @@ private final class ClassDefChecker(classDef: ClassDef, checkTree(receiver, unrestrictedEnv) // check that the This itself is valid - if (!postOptimizer) { + if (!featureSet.supports(FeatureSet.RelaxedCtorBodies)) { if (!(cls == classDef.className || classDef.superClass.exists(_.name == cls))) { implicit val ctx = ErrorContext(delegateCtorCall) reportError( @@ -597,7 +599,7 @@ private final class ClassDefChecker(classDef: ClassDef, implicit ctx: ErrorContext): List[Tree] = { if (classDef.kind.hasModuleAccessor) { - if (postOptimizer) { + if (featureSet.supports(FeatureSet.RelaxedCtorBodies)) { /* If the super constructor call was inlined, the StoreModule can be anywhere. * Moreover, the optimizer can remove StoreModules altogether in many cases. */ @@ -673,7 +675,7 @@ private final class ClassDefChecker(classDef: ClassDef, case Assign(lhs, rhs) => lhs match { case Select(This(), field) if env.isThisRestricted => - if (postOptimizer || field.name.className == classDef.className) + if (featureSet.supports(FeatureSet.RelaxedCtorBodies) || field.name.className == classDef.className) checkTree(lhs, env.withIsThisRestricted(false)) else checkTree(lhs, env) @@ -819,11 +821,13 @@ private final class ClassDefChecker(classDef: ClassDef, checkTree(index, env) case RecordSelect(record, _) => - checkAllowTransients() + if (!featureSet.supports(FeatureSet.Records)) + reportError("invalid use of records") checkTree(record, env) case RecordValue(_, elems) => - checkAllowTransients() + if (!featureSet.supports(FeatureSet.Records)) + reportError("invalid use of records") checkTrees(elems, env) case IsInstanceOf(expr, testType) => @@ -987,7 +991,9 @@ private final class ClassDefChecker(classDef: ClassDef, checkTrees(captureValues, env) case Transient(transient) => - checkAllowTransients() + if (!featureSet.supports(FeatureSet.OptimizedTransients)) + reportError(i"invalid transient tree of class ${transient.getClass().getName()}") + transient.traverse(new Traversers.Traverser { override def traverse(tree: Tree): Unit = checkTree(tree, env) }) @@ -996,11 +1002,6 @@ private final class ClassDefChecker(classDef: ClassDef, newEnv } - private def checkAllowTransients()(implicit ctx: ErrorContext): Unit = { - if (!postOptimizer) - reportError("invalid transient tree") - } - private def checkArrayType(tpe: ArrayType)( implicit ctx: ErrorContext): Unit = { checkArrayTypeRef(tpe.arrayTypeRef) @@ -1036,13 +1037,13 @@ object ClassDefChecker { * * @return Count of IR checking errors (0 in case of success) */ - def check(classDef: ClassDef, postBaseLinker: Boolean, postOptimizer: Boolean, logger: Logger): Int = { + def check(classDef: ClassDef, previousPhase: CheckingPhase, logger: Logger): Int = { val reporter = new LoggerErrorReporter(logger) - new ClassDefChecker(classDef, postBaseLinker, postOptimizer, reporter).checkClassDef() + new ClassDefChecker(classDef, previousPhase, reporter).checkClassDef() reporter.errorCount } - def check(linkedClass: LinkedClass, postOptimizer: Boolean, logger: Logger): Int = { + def check(linkedClass: LinkedClass, previousPhase: CheckingPhase, logger: Logger): Int = { // Rebuild a ClassDef out of the LinkedClass import linkedClass._ implicit val pos = linkedClass.pos @@ -1063,7 +1064,7 @@ object ClassDefChecker { topLevelExportDefs = Nil )(optimizerHints) - check(classDef, postBaseLinker = true, postOptimizer, logger) + check(classDef, previousPhase, logger) } private class Env( 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 new file mode 100644 index 0000000000..73f89b0d8b --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/FeatureSet.scala @@ -0,0 +1,76 @@ +/* + * 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.checker + +import org.scalajs.linker.checker.CheckingPhase._ + +/** A set of conditional IR features that the checkers can accept. + * + * At any given phase, the `ClassDefChecker` and the `IRChecker` must agree on + * the set of IR features that are valid. A `FeatureSet` factors out the + * knowledge of what feature is acceptable when. + */ +private[checker] final class FeatureSet private (private val flags: Int) extends AnyVal { + /** Does this feature set support (all of) the given feature set. */ + def supports(features: FeatureSet): Boolean = + (features.flags & flags) == features.flags + + /** The union of this feature set and `that` feature set. */ + def |(that: FeatureSet): FeatureSet = + new FeatureSet(this.flags | that.flags) +} + +private[checker] object FeatureSet { + /** Empty feature set. */ + val Empty = new FeatureSet(0) + + // Individual features + + /** Optional constructors in module classes and JS classes. */ + val OptionalConstructors = new FeatureSet(1 << 0) + + /** Explicit reflective proxy definitions. */ + val ReflectiveProxies = new FeatureSet(1 << 1) + + /** Transients that are the result of optimizations. */ + val OptimizedTransients = new FeatureSet(1 << 2) + + /** Records and record types. */ + val Records = new FeatureSet(1 << 3) + + /** Relaxed constructor discipline. + * + * - Optional super/delegate constructor call. + * - Delegate constructor calls can target any super class. + * - `this.x = ...` assignments before the delegate call can assign super class fields. + * - `StoreModule` can be anywhere, or not be there at all. + */ + val RelaxedCtorBodies = new FeatureSet(1 << 4) + + // Common sets + + /** Features introduced by the base linker. */ + private val Linked = + OptionalConstructors | ReflectiveProxies + + /** IR that is only the result of optimizations. */ + private val Optimized = + OptimizedTransients | Records | RelaxedCtorBodies + + /** The set of features allowed as output of the given phase. */ + def allowedAfter(phase: CheckingPhase): FeatureSet = phase match { + case Compiler => Empty + case BaseLinker => Linked + case Optimizer => Linked | Optimized + } +} 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 a749e4807e..de827f396a 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 @@ -29,11 +29,13 @@ import org.scalajs.linker.checker.ErrorReporter._ /** Checker for the validity of the IR. */ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, - postOptimizer: Boolean) { + previousPhase: CheckingPhase) { import IRChecker._ import reporter.reportError + private val featureSet = FeatureSet.allowedAfter(previousPhase) + private val classes: mutable.Map[ClassName, CheckedClass] = { val tups = for (classDef <- unit.classDefs) yield { implicit val ctx = ErrorContext(classDef) @@ -239,7 +241,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, case Assign(lhs, rhs) => def checkNonStaticField(receiver: Tree, name: FieldName): Unit = { receiver match { - case This() if (postOptimizer && env.inConstructorOf.isDefined) || + case This() if (featureSet.supports(FeatureSet.RelaxedCtorBodies) && env.inConstructorOf.isDefined) || env.inConstructorOf == Some(name.className) => /* ctors can write immutable fields of the class they are constructing. * postOptimizer, due to ctor inlining, we may write immutable parent class fields as well. @@ -717,12 +719,14 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheckExpect(value, env, ctpe) } - case Transient(transient) if postOptimizer => + case Transient(transient) if featureSet.supports(FeatureSet.OptimizedTransients) => + // No precise rules, but at least check that its children type-check on their own transient.traverse(new Traversers.Traverser { override def traverse(tree: Tree): Unit = typecheck(tree, env) }) - case RecordSelect(record, SimpleFieldIdent(fieldName)) if postOptimizer => + case RecordSelect(record, SimpleFieldIdent(fieldName)) + if featureSet.supports(FeatureSet.Records) => record.tpe match { case NothingType => // ok @@ -742,7 +746,8 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheck(record, env) - case RecordValue(RecordType(fields), elems) if postOptimizer => + case RecordValue(RecordType(fields), elems) + if featureSet.supports(FeatureSet.Records) => if (fields.size == elems.size) { for ((field, elem) <- fields.zip(elems)) typecheckExpect(elem, env, field.tpe) @@ -923,9 +928,9 @@ object IRChecker { * * @return Count of IR checking errors (0 in case of success) */ - def check(unit: LinkingUnit, logger: Logger, postOptimizer: Boolean = false): Int = { + def check(unit: LinkingUnit, logger: Logger, previousPhase: CheckingPhase): Int = { val reporter = new LoggerErrorReporter(logger) - new IRChecker(unit, reporter, postOptimizer).check() + new IRChecker(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 98b3fdf0de..ddf5b71f14 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 @@ -36,8 +36,10 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { import BaseLinker._ private val irLoader = new FileIRLoader - private val analyzer = - new Analyzer(config, initial = true, checkIR = checkIR, failOnError = true, irLoader) + private val analyzer = { + val checkIRFor = if (checkIR) Some(CheckingPhase.Compiler) else None + new Analyzer(config, initial = true, checkIRFor, failOnError = true, irLoader) + } private val methodSynthesizer = new MethodSynthesizer(irLoader) def link(irInput: Seq[IRFile], @@ -56,7 +58,7 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { } yield { if (checkIR) { logger.time("Linker: Check IR") { - val errorCount = IRChecker.check(linkResult, logger) + val errorCount = IRChecker.check(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/Refiner.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala index a263a7b388..0f074adf55 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 @@ -20,7 +20,7 @@ import org.scalajs.ir.Trees.ClassDef import org.scalajs.logging._ -import org.scalajs.linker.checker.IRChecker +import org.scalajs.linker.checker._ import org.scalajs.linker.interface.ModuleInitializer import org.scalajs.linker.standard._ import org.scalajs.linker.standard.ModuleSet.ModuleID @@ -31,8 +31,10 @@ final class Refiner(config: CommonPhaseConfig, checkIR: Boolean) { import Refiner._ private val irLoader = new ClassDefIRLoader - private val analyzer = - new Analyzer(config, initial = false, checkIR = checkIR, failOnError = true, irLoader) + private val analyzer = { + val checkIRFor = if (checkIR) Some(CheckingPhase.Optimizer) else None + new Analyzer(config, initial = false, checkIRFor, failOnError = true, irLoader) + } /* TODO: Remove this and replace with `checkIR` once the optimizer generates * well-typed IR with runtime longs. @@ -79,7 +81,7 @@ final class Refiner(config: CommonPhaseConfig, checkIR: Boolean) { if (shouldRunIRChecker) { logger.time("Refiner: Check IR") { - val errorCount = IRChecker.check(result, logger, postOptimizer = true) + val errorCount = IRChecker.check(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/test/scala/org/scalajs/linker/BaseLinkerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala index 1997d0f789..22033b7d3c 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala @@ -24,7 +24,7 @@ import org.scalajs.junit.async._ import org.scalajs.logging._ -import org.scalajs.linker.checker.ClassDefChecker +import org.scalajs.linker.checker._ import org.scalajs.linker.interface.StandardConfig import org.scalajs.linker.standard._ @@ -86,7 +86,8 @@ class BaseLinkerTest { for (moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers, config = config)) yield { val clazz = findClass(moduleSet, BoxedIntegerClass).get - val errorCount = ClassDefChecker.check(clazz, postOptimizer = false, + val previousPhase = CheckingPhase.BaseLinker + val errorCount = ClassDefChecker.check(clazz, previousPhase, new ScalaConsoleLogger(Level.Error)) assertEquals(0, errorCount) } 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 2d22331cc0..8bbf5ff00a 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 @@ -257,7 +257,7 @@ class ClassDefCheckerTest { ) ), "reflective profixes are only allowed in the public namespace", - allowReflectiveProxies = true + previousPhase = CheckingPhase.BaseLinker ) } @@ -817,13 +817,13 @@ class ClassDefCheckerTest { assertError( mainTestClassDef(Assign(RecordSelect(int(5), "i")(IntType), int(6))), "Assignment to RecordSelect of illegal tree: org.scalajs.ir.Trees$IntLiteral", - allowTransients = true) + previousPhase = CheckingPhase.Optimizer) } } private object ClassDefCheckerTest { private def assertError(clazz: ClassDef, expectMsg: String, - allowReflectiveProxies: Boolean = false, allowTransients: Boolean = false) = { + previousPhase: CheckingPhase = CheckingPhase.Compiler): Unit = { var seen = false val reporter = new ErrorReporter { def reportError(msg: String)(implicit ctx: ErrorReporter.ErrorContext) = { @@ -833,7 +833,7 @@ private object ClassDefCheckerTest { } } - new ClassDefChecker(clazz, allowReflectiveProxies, allowTransients, reporter).checkClassDef() + new ClassDefChecker(clazz, previousPhase, reporter).checkClassDef() assertTrue("no errors reported", seen) } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala index f3854a7a76..68a59755ba 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala @@ -20,6 +20,7 @@ import org.scalajs.logging._ import org.scalajs.linker._ import org.scalajs.linker.analyzer._ +import org.scalajs.linker.checker.CheckingPhase import org.scalajs.linker.frontend.FileIRLoader import org.scalajs.linker.interface._ import org.scalajs.linker.standard._ @@ -101,8 +102,9 @@ object LinkingUtils { val injectedIRFiles = StandardLinkerBackend(config).injectedIRFiles val irLoader = new FileIRLoader + val checkIRFor = Some(CheckingPhase.Compiler) val analyzer = new Analyzer(CommonPhaseConfig.fromStandardConfig(config), - initial = true, checkIR = true, failOnError = false, irLoader) + initial = true, checkIRFor, failOnError = false, irLoader) val logger = new ScalaConsoleLogger(Level.Error) for { From ca39c30dd73285545293d285835df3054b778479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 31 Jan 2025 17:13:52 +0100 Subject: [PATCH 04/30] Remove ancient codegen opt for expanded Scala anon function classes. Since the very early days of Scala.js, we have had a code size optimization for lambdas expanded as anonymous classes. We tried very hard to recover the captures from fields, and the body of the `apply` method, in order to reconstruct a `js.Closure`. That made sense as long as we supported Scala 2.11 (and before that, 2.10). However, since Scala 2.12.x, scalac almost never expands lambdas of `scala.FunctionN`. Instead, they reach the backend as `Function` node. The only situations where scalac still expands lambdas as anonymous classes is if it used in an argument to a super constructor (or delegate `this()` constructor). The code paths for that code size optimization are therefore almost dead code, and do not pull their weight anymore. It was, AFAICT, the only place where we still *tried* to emit something a certain way, with a fallback. This commit removes the optimization and all the related infrastructure. --- .../org/scalajs/nscplugin/GenJSCode.scala | 187 ++++-------------- .../scalajs/nscplugin/test/JSSAMTest.scala | 4 +- 2 files changed, 39 insertions(+), 152 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 0ad5ed423e..60cea646b0 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -204,8 +204,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } // For anonymous methods - // These have a default, since we always read them. - private val tryingToGenMethodAsJSFunction = new ScopedVar[Boolean](false) + // It has a default, since we always read it. private val paramAccessorLocals = new ScopedVar(Map.empty[Symbol, js.ParamDef]) /* Contextual JS class value for some operations of nested JS classes that @@ -223,11 +222,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } - private class CancelGenMethodAsJSFunction(message: String) - extends scala.util.control.ControlThrowable { - override def getMessage(): String = message - } - // Rewriting of anonymous function classes --------------------------------- /** Start nested generation of a class. @@ -248,7 +242,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) undefinedDefaultParams := null, mutableLocalVars := null, mutatedLocalVars := null, - tryingToGenMethodAsJSFunction := false, paramAccessorLocals := Map.empty )(withNewLocalNameScope(body)) } @@ -387,21 +380,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private val generatedStaticForwarderClasses = ListBuffer.empty[(Symbol, js.ClassDef)] private def consumeLazilyGeneratedAnonClass(sym: Symbol): ClassDef = { - /* If we are trying to generate an method as JSFunction, we cannot - * actually consume the symbol, since we might fail trying and retry. - * We will then see the same tree again and not find the symbol anymore. - * - * If we are sure this is the only generation, we remove the symbol to - * make sure we don't generate the same class twice. - */ - val optDef = { - if (tryingToGenMethodAsJSFunction) - lazilyGeneratedAnonClasses.get(sym) - else - lazilyGeneratedAnonClasses.remove(sym) - } - - optDef.getOrElse { + lazilyGeneratedAnonClasses.remove(sym).getOrElse { abort("Couldn't find tree for lazily generated anonymous class " + s"${sym.fullName} at ${sym.pos}") } @@ -450,25 +429,22 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } val allClassDefs = collectClassDefs(cunit.body) - /* There are three types of anonymous classes we want to generate - * only once we need them so we can inline them at construction site: + /* There are two types of anonymous classes we want to generate only + * once we need them, so we can inline them at construction site: * - * - anonymous class that are JS types, which includes: - * - lambdas for js.FunctionN and js.ThisFunctionN (SAMs). (We may - * not generate actual Scala classes for these). - * - anonymous (non-lambda) JS classes. These classes may not have - * their own prototype. Therefore, their constructor *must* be - * inlined. - * - lambdas for scala.FunctionN. This is only an optimization and may - * fail. In the case of failure, we fall back to generating a - * fully-fledged Scala class. + * - Lambdas for `js.Function` SAMs, including `js.FunctionN`, + * `js.ThisFunctionN` and custom JS function SAMs. We must generate + * JS functions for these, instead of actual classes. + * - Anonymous (non-lambda) JS classes. These classes may not have + * their own prototype. Therefore, their constructor *must* be + * inlined. * * Since for all these, we don't know how they inter-depend, we just * store them in a map at this point. */ val (lazyAnons, fullClassDefs) = allClassDefs.partition { cd => val sym = cd.symbol - isAnonymousJSClass(sym) || isJSFunctionDef(sym) || sym.isAnonymousFunction + isAnonymousJSClass(sym) || isJSFunctionDef(sym) } lazilyGeneratedAnonClasses ++= lazyAnons.map(cd => cd.symbol -> cd) @@ -2824,9 +2800,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) */ private def genThis()(implicit pos: Position): js.Tree = { thisLocalVarName.fold[js.Tree] { - if (tryingToGenMethodAsJSFunction) { - throw new CancelGenMethodAsJSFunction( - "Trying to generate `this` inside the body") + if (isJSFunctionDef(currentClassSym)) { + abort( + "Unexpected `this` reference inside the body of a JS function class: " + + currentClassSym.fullName) } js.This()(currentThisType) } { thisLocalName => @@ -3359,10 +3336,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } /** Gen JS code for a constructor call (new). + * * Further refined into: - * * new String(...) * * new of a hijacked boxed class - * * new of an anonymous function class that was recorded as JS function + * * new of a JS function class * * new of a JS class * * new Array * * regular new @@ -3382,13 +3359,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } else if (isJSFunctionDef(clsSym)) { val classDef = consumeLazilyGeneratedAnonClass(clsSym) genJSFunction(classDef, args.map(genExpr)) - } else if (clsSym.isAnonymousFunction) { - val classDef = consumeLazilyGeneratedAnonClass(clsSym) - tryGenAnonFunctionClass(classDef, args.map(genExpr)).getOrElse { - // Cannot optimize anonymous function class. Generate full class. - generatedClasses += nestedGenerateClass(clsSym)(genClass(classDef)) -> clsSym.pos - genNew(clsSym, ctor, genActualArgs(ctor, args)) - } } else if (isJSType(clsSym)) { genPrimitiveJSNew(tree) } else { @@ -6057,69 +6027,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Synthesizers for JS functions ------------------------------------------- - /** Try and generate JS code for an anonymous function class. - * - * Returns Some() if the class could be rewritten that way, None - * otherwise. - * - * We make the following assumptions on the form of such classes: - * - It is an anonymous function - * - Includes being anonymous, final, and having exactly one constructor - * - It is not a PartialFunction - * - It has no field other than param accessors - * - It has exactly one constructor - * - It has exactly one non-bridge method apply if it is not specialized, - * or a method apply$...$sp and a forwarder apply if it is specialized. - * - As a precaution: it is synthetic - * - * From a class looking like this: - * - * final class (outer, capture1, ..., captureM) extends AbstractionFunctionN[...] { - * def apply(param1, ..., paramN) = { - * - * } - * } - * new (o, c1, ..., cM) - * - * we generate a function: - * - * arrow-lambda(param1, ..., paramN) { - * - * } - * - * so that, at instantiation point, we can write: - * - * new AnonFunctionN(function) - * - * the latter tree is returned in case of success. - * - * Trickier things apply when the function is specialized. - */ - private def tryGenAnonFunctionClass(cd: ClassDef, - capturedArgs: List[js.Tree]): Option[js.Tree] = { - // scalastyle:off return - implicit val pos = cd.pos - val sym = cd.symbol - assert(sym.isAnonymousFunction, - s"tryGenAndRecordAnonFunctionClass called with non-anonymous function $cd") - - if (!sym.superClass.fullName.startsWith("scala.runtime.AbstractFunction")) { - /* This is an anonymous class for a non-LMF capable SAM in 2.12. - * We must not rewrite it, as it would then not inherit from the - * appropriate parent class and/or interface. - */ - None - } else { - nestedGenerateClass(sym) { - val (functionBase, arity) = - tryGenAnonFunctionClassGeneric(cd, capturedArgs)(_ => return None) - - Some(genJSFunctionToScala(functionBase, arity)) - } - } - // scalastyle:on return - } - /** Gen a conversion from a JavaScript function into a Scala function. */ private def genJSFunctionToScala(jsFunction: js.Tree, arity: Int)( implicit pos: Position): js.Tree = { @@ -6136,11 +6043,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * functions are not classes, we deconstruct the ClassDef, then * reconstruct it to be a genuine Closure. * - * Compared to `tryGenAnonFunctionClass()`, this function must - * always succeed, because we really cannot afford keeping them as - * anonymous classes. The good news is that it can do so, because the - * body of SAM lambdas is hoisted in the enclosing class. Hence, the - * apply() method is just a forwarder to calling that hoisted method. + * We can always do so, because the body of SAM lambdas is hoisted in the + * enclosing class. Hence, the apply() method is just a forwarder to + * calling that hoisted method. * * From a class looking like this: * @@ -6163,26 +6068,18 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) s"genAndRecordJSFunctionClass called with non-JS function $cd") nestedGenerateClass(sym) { - val (function, _) = tryGenAnonFunctionClassGeneric(cd, captures)(msg => - abort(s"Could not generate function for JS function: $msg")) - - function + genJSFunctionInner(cd, captures) } } - /** Code common to tryGenAndRecordAnonFunctionClass and - * genAndRecordJSFunctionClass. - */ - private def tryGenAnonFunctionClassGeneric(cd: ClassDef, - initialCapturedArgs: List[js.Tree])( - fail: (=> String) => Nothing): (js.Tree, Int) = { + /** The code of `genJSFunction` that is inside the `nestedGenerateClass` wrapper. */ + private def genJSFunctionInner(cd: ClassDef, + initialCapturedArgs: List[js.Tree]): js.Closure = { implicit val pos = cd.pos val sym = cd.symbol - // First checks - - if (sym.isSubClass(PartialFunctionClass)) - fail(s"Cannot rewrite PartialFunction $cd") + def fail(reason: String): Nothing = + abort(s"Could not generate function for JS function: $reason") // First step: find the apply method def, and collect param accessors @@ -6210,10 +6107,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (!ddsym.isPrimaryConstructor) fail(s"Non-primary constructor $ddsym in anon function $cd") } else { - val name = dd.name.toString - if (name == "apply" || (ddsym.isSpecialized && name.startsWith("apply$"))) { - if ((applyDef eq null) || ddsym.isSpecialized) + if (dd.name == nme.apply) { + if (!ddsym.isBridge) { + if (applyDef ne null) + fail(s"Found duplicate apply method $ddsym in $cd") applyDef = dd + } } else if (ddsym.hasAnnotation(JSOptionalAnnotation)) { // Ignore (this is useful for default parameters in custom JS function types) } else { @@ -6253,24 +6152,15 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Third step: emit the body of the apply method def val applyMethod = withScopedVars( - paramAccessorLocals := (paramAccessors zip ctorParamDefs).toMap, - tryingToGenMethodAsJSFunction := true + paramAccessorLocals := (paramAccessors zip ctorParamDefs).toMap ) { - try { - genMethodWithCurrentLocalNameScope(applyDef) - } catch { - case e: CancelGenMethodAsJSFunction => - fail(e.getMessage) - } + genMethodWithCurrentLocalNameScope(applyDef) } // Fourth step: patch the body to unbox parameters and box result - val hasRepeatedParam = { - sym.superClass == JSFunctionClass && // Scala functions are known not to have repeated params - enteringUncurry { - applyDef.symbol.paramss.flatten.lastOption.exists(isRepeated(_)) - } + val hasRepeatedParam = enteringUncurry { + applyDef.symbol.paramss.flatten.lastOption.exists(isRepeated(_)) } val js.MethodDef(_, _, _, params, _, body) = applyMethod @@ -6303,8 +6193,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val ok = patchedParams.nonEmpty if (!ok) { reporter.error(pos, - "The SAM or apply method for a js.ThisFunction must have a " + - "leading non-varargs parameter") + "The apply method for a js.ThisFunction must have a leading non-varargs parameter") } ok } @@ -6335,9 +6224,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } - val arity = params.size - - (closure, arity) + closure } } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala index 3513fff2e3..4eedcb3447 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala @@ -126,10 +126,10 @@ class JSSAMTest extends DirectTest with TestHelpers { } """ hasErrors """ - |newSource1.scala:14: error: The SAM or apply method for a js.ThisFunction must have a leading non-varargs parameter + |newSource1.scala:14: error: The apply method for a js.ThisFunction must have a leading non-varargs parameter | val badThisFunction1: BadThisFunction1 = () => 42 | ^ - |newSource1.scala:15: error: The SAM or apply method for a js.ThisFunction must have a leading non-varargs parameter + |newSource1.scala:15: error: The apply method for a js.ThisFunction must have a leading non-varargs parameter | val badThisFunction2: BadThisFunction2 = args => args.size | ^ """ From 0181706748268e4e33cfc3b3c5e3438702732e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 4 Feb 2025 09:51:36 +0100 Subject: [PATCH 05/30] Wasm: Remove dead code jsLinkingInfo object. This was forgotten in 7bf410dd922662dabfc85f1d76d40431c1fe0910. --- .../linker/backend/wasmemitter/Emitter.scala | 20 ------------------- .../backend/wasmemitter/LoaderContent.scala | 3 +-- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala index fcbe987f50..51d1c99fd0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala @@ -259,25 +259,6 @@ final class Emitter(config: Emitter.Config) { implicit val noPos = Position.NoPosition - // Linking info - val linkingInfo = { - // must be in sync with scala.scalajs.runtime.LinkingInfo - import config.coreSpec._ - import js.{BooleanLiteral => bool, IntLiteral => int, StringLiteral => str} - - def objectFreeze(tree: js.Tree): js.Tree = - js.Apply(js.DotSelect(js.VarRef(js.Ident("Object")), js.Ident("freeze")), tree :: Nil) - - objectFreeze(js.ObjectConstr(List( - str("esVersion") -> int(esFeatures.esVersion.edition), - str("assumingES6") -> bool(esFeatures.useECMAScript2015Semantics), // different name for historical reasons - str("isWebAssembly") -> bool(true), - str("productionMode") -> bool(semantics.productionMode), - str("linkerVersion") -> str(ScalaJSVersions.current), - str("fileLevelThis") -> js.This() - ))) - } - // Sort for stability val importedModules = module.externalDependencies.toList.sorted @@ -326,7 +307,6 @@ final class Emitter(config: Emitter.Config) { js.VarRef(loadFunIdent), List( js.StringLiteral(config.internalWasmFileURIPattern(module.id)), - linkingInfo, exportSettersDict, customJSHelpersDict ) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala index a096a66c5d..271894e7f5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala @@ -180,10 +180,9 @@ const stringBuiltinPolyfills = { equals: (a, b) => a === b, }; -export async function load(wasmFileURL, linkingInfo, exportSetters, customJSHelpers) { +export async function load(wasmFileURL, exportSetters, customJSHelpers) { const myScalaJSHelpers = { ...scalaJSHelpers, - jsLinkingInfo: linkingInfo, idHashCodeMap: new WeakMap() }; const importsObj = { From bdd73fb9ea90c9cf0d15773d64bb42fb3a18312d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 31 Jan 2025 14:16:57 +0100 Subject: [PATCH 06/30] Make the guards for deserialization hacks clearer. Previously, deserialization hacks were guarded by the last IR version to which they applied. This was confusing when IR versions skipped one or more minor number, for example from 1.8 to 1.11. The version that introduces the hack was 1.11, but the guards were `hacks.use8`. Comments and error messages were typically disagreeing with that convention. For example, in that situation, they would refer to "prior to 1.11" or "1.11 introduced". That was another mismatch between the programmatic guards and the text in the comments. Overall this was very confusing. Now, we refer to the *first* IR version to which a hack must *not* be applied. This corresponds to the comments, and in general to the IR version that introduced the hack. --- In addition, we take the opportunity to use a unique `useBelow(V)` instead of individual `useV` methods. The fixed list of `useV` methods with their corresponding string versions had become too long. --- .../scala/org/scalajs/ir/Serializers.scala | 156 ++++++++---------- .../org/scalajs/ir/SerializersTest.scala | 61 +++++++ project/BinaryIncompatibilities.scala | 3 + 3 files changed, 135 insertions(+), 85 deletions(-) create mode 100644 ir/shared/src/test/scala/org/scalajs/ir/SerializersTest.scala 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 4866e36457..dbd8a03f83 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -41,6 +41,14 @@ object Serializers { */ final val IRMagicNumber = 0xCAFE4A53 + /** A regex for a compatible stable binary IR version from which we may need + * to migrate things with hacks. + */ + private val CompatibleStableIRVersionRegex = { + val prefix = java.util.regex.Pattern.quote(ScalaJSVersions.binaryCross + ".") + new scala.util.matching.Regex(prefix + "(\\d+)") + } + // For deserialization hack private final val DynamicImportThunkClass = ClassName("scala.scalajs.runtime.DynamicImportThunk") @@ -1107,7 +1115,7 @@ object Serializers { case TagAssign => val lhs0 = readTree() - val lhs = if (hacks.use4 && lhs0.tpe == NothingType) { + val lhs = if (hacks.useBelow(5) && lhs0.tpe == NothingType) { /* Note [Nothing FieldDef rewrite] * (throw qual.field[null]) = rhs --> qual.field[null] = rhs */ @@ -1128,7 +1136,7 @@ object Serializers { case TagWhile => While(readTree(), readTree()) case TagDoWhile => - if (!hacks.use12) + if (!hacks.useBelow(13)) throw new IOException(s"Found invalid pre-1.13 DoWhile loop at $pos") // Rewrite `do { body } while (cond)` to `while ({ body; cond }) {}` val body = readTree() @@ -1153,7 +1161,7 @@ object Serializers { case TagLoadModule => LoadModule(readClassName()) case TagStoreModule => - if (hacks.use13) { + if (hacks.useBelow(16)) { val cls = readClassName() val rhs = readTree() rhs match { @@ -1172,7 +1180,7 @@ object Serializers { val field = readFieldIdent() val tpe = readType() - if (hacks.use4 && tpe == NothingType) { + if (hacks.useBelow(5) && tpe == NothingType) { /* Note [Nothing FieldDef rewrite] * qual.field[nothing] --> throw qual.field[null] */ @@ -1217,7 +1225,7 @@ object Serializers { case TagArrayLength | TagGetClass | TagClone | TagIdentityHashCode | TagWrapAsThrowable | TagUnwrapFromThrowable | TagThrow => - if (!hacks.use17) { + if (!hacks.useBelow(18)) { throw new IOException( s"Illegal legacy node $tag found in class ${enclosingClassName.nameString}") } @@ -1240,7 +1248,7 @@ object Serializers { UnaryOp(UnaryOp.UnwrapFromThrowable, checkNotNullLhs) case TagThrow => val patchedLhs = - if (hacks.use8) throwArgumentHack8(lhs) + if (hacks.useBelow(11)) throwArgumentHackBelow11(lhs) else lhs UnaryOp(UnaryOp.Throw, patchedLhs) } @@ -1253,7 +1261,7 @@ object Serializers { NewArray(arrayTypeRef, length) case _ => - if (hacks.use16) { + if (hacks.useBelow(17)) { // Rewrite as a call to j.l.r.Array.newInstance val ArrayTypeRef(base, origDims) = arrayTypeRef val newDims = origDims - lengths.size @@ -1284,7 +1292,7 @@ object Serializers { case TagIsInstanceOf => val expr = readTree() val testType0 = readType() - val testType = if (hacks.use16) { + val testType = if (hacks.useBelow(17)) { testType0 match { case ClassType(className, true) => ClassType(className, nullable = false) case ArrayType(arrayTypeRef, true) => ArrayType(arrayTypeRef, nullable = false) @@ -1302,7 +1310,7 @@ object Serializers { case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent()) case TagJSSelect => - if (hacks.use17 && buf.get(buf.position()) == TagJSLinkingInfo) { + if (hacks.useBelow(18) && buf.get(buf.position()) == TagJSLinkingInfo) { val jsLinkingInfo = readTree() readTree() match { case StringLiteral("productionMode") => @@ -1345,7 +1353,7 @@ object Serializers { case TagJSTypeOfGlobalRef => JSTypeOfGlobalRef(readTree().asInstanceOf[JSGlobalRef]) case TagJSLinkingInfo => - if (hacks.use17) { + if (hacks.useBelow(18)) { JSObjectConstr(List( (StringLiteral("productionMode"), LinkTimeProperty(ProductionMode)(BooleanType)), (StringLiteral("esVersion"), LinkTimeProperty(ESVersion)(IntType)), @@ -1374,7 +1382,7 @@ object Serializers { case TagVarRef => val name = - if (hacks.use17) readLocalIdent().name + if (hacks.useBelow(18)) readLocalIdent().name else readLocalName() VarRef(name)(readType()) @@ -1409,7 +1417,7 @@ object Serializers { } } - /** Patches the argument of a `Throw` for IR version until 1.8. + /** Patches the argument of a `Throw` for IR version below 1.11. * * Prior to Scala.js 1.11, `Throw(e)` was emitted by the compiler with * the somewhat implied assumption that it would "throw an NPE" (but @@ -1443,7 +1451,7 @@ object Serializers { * `AnyType`. We can accurately use that test to know whether we need to * apply the patch. */ - private def throwArgumentHack8(expr: Tree)(implicit pos: Position): Tree = { + private def throwArgumentHackBelow11(expr: Tree)(implicit pos: Position): Tree = { if (expr.tpe == AnyType) expr else if (!expr.tpe.isNullable) @@ -1465,11 +1473,11 @@ object Serializers { val originalName = readOriginalName() val kind = ClassKind.fromByte(readByte()) - if (hacks.use16) { + if (hacks.useBelow(17)) { thisTypeForHack = kind match { case ClassKind.Class | ClassKind.ModuleClass | ClassKind.Interface => Some(ClassType(cls, nullable = false)) - case ClassKind.HijackedClass if hacks.use8 => + case ClassKind.HijackedClass if hacks.useBelow(11) => // Use getOrElse as safety guard for otherwise invalid inputs Some(BoxedClassToPrimType.getOrElse(cls, ClassType(cls, nullable = false))) case _ => @@ -1484,7 +1492,7 @@ object Serializers { val superClass = readOptClassIdent() val parents = readClassIdents() - if (hacks.use17 && kind.isClass) { + if (hacks.useBelow(18) && kind.isClass) { /* In 1.18, we started enforcing the constructor chaining discipline. * Unfortunately, we used to generate a wrong super constructor call in * synthetic classes extending `DynamicImportThunk`, so we patch them. @@ -1493,7 +1501,7 @@ object Serializers { superClass.exists(_.name == DynamicImportThunkClass) } - /* jsSuperClass is not hacked like in readMemberDef.bodyHack5. The + /* jsSuperClass is not hacked like in readMemberDef.bodyHackBelow6. The * compilers before 1.6 always use a simple VarRef() as jsSuperClass, * when there is one, so no hack is required. */ @@ -1528,22 +1536,22 @@ object Serializers { val methods = { val methods0 = methodsBuilder.result() - if (hacks.use4 && kind.isJSClass) { + if (hacks.useBelow(5) && kind.isJSClass) { // #4409: Filter out abstract methods in non-native JS classes for version < 1.5 methods0.filter(_.body.isDefined) - } else if (hacks.use16 && cls == ClassClass) { - jlClassMethodsHack16(methods0) - } else if (hacks.use16 && cls == HackNames.ReflectArrayModClass) { - jlReflectArrayMethodsHack16(methods0) + } else if (hacks.useBelow(17) && cls == ClassClass) { + jlClassMethodsHackBelow17(methods0) + } else if (hacks.useBelow(17) && cls == HackNames.ReflectArrayModClass) { + jlReflectArrayMethodsHackBelow17(methods0) } else { methods0 } } val (jsConstructor, jsMethodProps) = { - if (hacks.use8 && kind.isJSClass) { - assert(jsConstructorBuilder.result().isEmpty, "found JSConstructorDef in pre 1.8 IR") - jsConstructorHack(kind, jsMethodPropsBuilder.result()) + if (hacks.useBelow(11) && kind.isJSClass) { + assert(jsConstructorBuilder.result().isEmpty, "found JSConstructorDef in pre 1.11 IR") + jsConstructorHackBelow11(kind, jsMethodPropsBuilder.result()) } else { (jsConstructorBuilder.result(), jsMethodPropsBuilder.result()) } @@ -1557,7 +1565,7 @@ object Serializers { optimizerHints) } - private def jlClassMethodsHack16(methods: List[MethodDef]): List[MethodDef] = { + private def jlClassMethodsHackBelow17(methods: List[MethodDef]): List[MethodDef] = { for (method <- methods) yield { implicit val pos = method.pos @@ -1621,7 +1629,7 @@ object Serializers { } } - private def jlReflectArrayMethodsHack16(methods: List[MethodDef]): List[MethodDef] = { + private def jlReflectArrayMethodsHackBelow17(methods: List[MethodDef]): List[MethodDef] = { /* Basically this method hard-codes new implementations for the two * overloads of newInstance. * It is horrible, but better than pollute everything else in the linker. @@ -1803,7 +1811,7 @@ object Serializers { newInstanceRecMethod :: newMethods } - private def jsConstructorHack(ownerKind: ClassKind, + private def jsConstructorHackBelow11(ownerKind: ClassKind, jsMethodProps: List[JSMethodPropDef]): (Option[JSConstructorDef], List[JSMethodPropDef]) = { val jsConstructorBuilder = new OptionBuilder[JSConstructorDef] val jsMethodPropsBuilder = List.newBuilder[JSMethodPropDef] @@ -1847,7 +1855,7 @@ object Serializers { val originalName = readOriginalName() val ftpe0 = readType() - val ftpe = if (hacks.use4 && ftpe0 == NothingType) { + val ftpe = if (hacks.useBelow(5) && ftpe0 == NothingType) { /* Note [Nothing FieldDef rewrite] * val field: nothing --> val field: null */ @@ -1881,7 +1889,7 @@ object Serializers { * rewrite it as a static initializers instead (``). */ val name0 = readMethodIdent() - if (hacks.use1 && + if (hacks.useBelow(2) && name0.name == ClassInitializerName && !ownerKind.isJSType) { MethodIdent(StaticInitializerName)(name0.pos) @@ -1896,11 +1904,11 @@ object Serializers { val body = readOptTree() val optimizerHints = OptimizerHints.fromBits(readInt()) - if (hacks.use0 && + if (hacks.useBelow(1) && flags.namespace == MemberNamespace.Public && owner == HackNames.SystemModule && name.name == HackNames.identityHashCodeName) { - /* #3976: 1.0 javalib relied on wrong linker dispatch. + /* #3976: Before 1.1, the javalib relied on wrong linker dispatch. * We simply replace it with a correct implementation. */ assert(args.size == 1) @@ -1910,7 +1918,7 @@ object Serializers { MethodDef(flags, name, originalName, args, resultType, patchedBody)( patchedOptimizerHints, optHash) - } else if (hacks.use4 && + } else if (hacks.useBelow(5) && flags.namespace == MemberNamespace.Public && owner == ObjectClass && name.name == HackNames.cloneName) { @@ -1943,7 +1951,7 @@ object Serializers { MethodDef(flags, name, originalName, args, resultType, patchedBody)( patchedOptimizerHints, optHash) } else { - val patchedBody = body.map(bodyHack5(_, isStat = resultType == VoidType)) + val patchedBody = body.map(bodyHackBelow6(_, isStat = resultType == VoidType)) MethodDef(flags, name, originalName, args, resultType, patchedBody)( optimizerHints, optHash) } @@ -1958,7 +1966,7 @@ object Serializers { assert(len >= 0) /* JSConstructorDef was introduced in 1.11. Therefore, by - * construction, they never need the body hack of 1.5. + * construction, they never need the body hack below 1.6. */ val flags = MemberFlags.fromBits(readInt()) @@ -1975,7 +1983,7 @@ object Serializers { private def maybeHackJSConstructorDefAfterSuper(ownerKind: ClassKind, afterSuper0: List[Tree], superCallPos: Position): List[Tree] = { - if (hacks.use17 && ownerKind == ClassKind.JSModuleClass) { + if (hacks.useBelow(18) && ownerKind == ClassKind.JSModuleClass) { afterSuper0 match { case StoreModule() :: _ => afterSuper0 case _ => StoreModule()(superCallPos) :: afterSuper0 @@ -1992,16 +2000,16 @@ object Serializers { assert(len >= 0) val flags = MemberFlags.fromBits(readInt()) - val name = bodyHack5Expr(readTree()) + val name = bodyHackBelow6Expr(readTree()) val (params, restParam) = readParamDefsWithRest() - val body = bodyHack5Expr(readTree()) + val body = bodyHackBelow6Expr(readTree()) JSMethodDef(flags, name, params, restParam, body)( OptimizerHints.fromBits(readInt()), optHash) } private def readJSPropertyDef()(implicit pos: Position): JSPropertyDef = { val optHash: Version = { - if (hacks.use12) { + if (hacks.useBelow(13)) { Unversioned } else { val optHash = readOptHash() @@ -2013,11 +2021,11 @@ object Serializers { } val flags = MemberFlags.fromBits(readInt()) - val name = bodyHack5Expr(readTree()) - val getterBody = readOptTree().map(bodyHack5Expr(_)) + val name = bodyHackBelow6Expr(readTree()) + val getterBody = readOptTree().map(bodyHackBelow6Expr(_)) val setterArgAndBody = { if (readBoolean()) - Some((readParamDef(), bodyHack5Expr(readTree()))) + Some((readParamDef(), bodyHackBelow6Expr(readTree()))) else None } @@ -2037,7 +2045,7 @@ object Serializers { * not derived from their children like Block or TryFinally, or * constant like While). */ - private object BodyHack5Transformer extends Transformers.Transformer { + private object BodyHackBelow6Transformer extends Transformers.Transformer { def transformStat(tree: Tree): Tree = { implicit val pos = tree.pos @@ -2090,11 +2098,11 @@ object Serializers { else transform(tree) } - private def bodyHack5(body: Tree, isStat: Boolean): Tree = - if (!hacks.use5) body - else BodyHack5Transformer.transform(body, isStat) + private def bodyHackBelow6(body: Tree, isStat: Boolean): Tree = + if (!hacks.useBelow(6)) body + else BodyHackBelow6Transformer.transform(body, isStat) - private def bodyHack5Expr(body: Tree): Tree = bodyHack5(body, isStat = false) + private def bodyHackBelow6Expr(body: Tree): Tree = bodyHackBelow6(body, isStat = false) def readTopLevelExportDef(): TopLevelExportDef = { implicit val pos = readPosition() @@ -2108,7 +2116,7 @@ object Serializers { } def readModuleID(): String = - if (hacks.use2) DefaultModuleID + if (hacks.useBelow(3)) DefaultModuleID else readString() (tag: @switch) match { @@ -2173,7 +2181,7 @@ object Serializers { val ptpe = readType() val mutable = readBoolean() - if (hacks.use4) { + if (hacks.useBelow(5)) { val rest = readBoolean() assert(!rest, "Illegal rest parameter") } @@ -2185,7 +2193,7 @@ object Serializers { List.fill(readInt())(readParamDef()) def readParamDefsWithRest(): (List[ParamDef], Option[ParamDef]) = { - if (hacks.use4) { + if (hacks.useBelow(5)) { val (params, isRest) = List.fill(readInt()) { implicit val pos = readPosition() (ParamDef(readLocalIdent(), readOriginalName(), readType(), readBoolean()), readBoolean()) @@ -2360,7 +2368,7 @@ object Serializers { /* Before 1.18, `LabelName`s were always wrapped in `LabelIdent`s, whose * encoding was a `Position` followed by the actual `LabelName`. */ - if (hacks.use17) + if (hacks.useBelow(18)) readPosition() // intentional discard val i = readInt() @@ -2476,41 +2484,19 @@ object Serializers { } } - /** Hacks for backwards compatible deserializing. */ - private final class Hacks(sourceVersion: String) { - val use0: Boolean = sourceVersion == "1.0" - - val use1: Boolean = use0 || sourceVersion == "1.1" - - val use2: Boolean = use1 || sourceVersion == "1.2" - - private val use3: Boolean = use2 || sourceVersion == "1.3" - - val use4: Boolean = use3 || sourceVersion == "1.4" - - val use5: Boolean = use4 || sourceVersion == "1.5" - - private val use6: Boolean = use5 || sourceVersion == "1.6" - - private val use7: Boolean = use6 || sourceVersion == "1.7" - - val use8: Boolean = use7 || sourceVersion == "1.8" - - assert(sourceVersion != "1.9", "source version 1.9 does not exist") - assert(sourceVersion != "1.10", "source version 1.10 does not exist") - - private val use11: Boolean = use8 || sourceVersion == "1.11" - - val use12: Boolean = use11 || sourceVersion == "1.12" - - val use13: Boolean = use12 || sourceVersion == "1.13" - - assert(sourceVersion != "1.14", "source version 1.14 does not exist") - assert(sourceVersion != "1.15", "source version 1.15 does not exist") - - val use16: Boolean = use13 || sourceVersion == "1.16" + /** Hacks for backwards compatible deserializing. + * + * `private[ir]` for testing purposes only. + */ + private[ir] final class Hacks(sourceVersion: String) { + private val fromVersion = sourceVersion match { + case CompatibleStableIRVersionRegex(minorDigits) => minorDigits.toInt + case _ => Int.MaxValue // never use any hack + } - val use17: Boolean = use16 || sourceVersion == "1.17" + /** Should we use the hacks to migrate from an IR version below `targetVersion`? */ + def useBelow(targetVersion: Int): Boolean = + fromVersion < targetVersion } /** Names needed for hacks. */ diff --git a/ir/shared/src/test/scala/org/scalajs/ir/SerializersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/SerializersTest.scala new file mode 100644 index 0000000000..a8c18507d9 --- /dev/null +++ b/ir/shared/src/test/scala/org/scalajs/ir/SerializersTest.scala @@ -0,0 +1,61 @@ +/* + * 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.ir + +import org.junit.Test +import org.junit.Assert._ + +class SerializersTest { + @Test def testHacksUseBelow(): Unit = { + import Serializers.Hacks + + val hacks1_0 = new Hacks("1.0") + assertFalse(hacks1_0.useBelow(0)) + assertTrue(hacks1_0.useBelow(1)) + assertTrue(hacks1_0.useBelow(5)) + assertTrue(hacks1_0.useBelow(15)) + assertTrue(hacks1_0.useBelow(1000)) + + val hacks1_7 = new Hacks("1.7") + assertFalse(hacks1_7.useBelow(0)) + assertFalse(hacks1_7.useBelow(1)) + assertFalse(hacks1_7.useBelow(5)) + assertFalse(hacks1_7.useBelow(7)) + assertTrue(hacks1_7.useBelow(8)) + assertTrue(hacks1_7.useBelow(15)) + assertTrue(hacks1_7.useBelow(1000)) + + val hacks1_50 = new Hacks("1.50") + assertFalse(hacks1_50.useBelow(0)) + assertFalse(hacks1_50.useBelow(1)) + assertFalse(hacks1_50.useBelow(5)) + assertFalse(hacks1_50.useBelow(15)) + assertTrue(hacks1_50.useBelow(1000)) + + // Non-stable versions never get any hacks + val hacks1_9_snapshot = new Hacks("1.9-SNAPSHOT") + assertFalse(hacks1_9_snapshot.useBelow(0)) + assertFalse(hacks1_9_snapshot.useBelow(1)) + assertFalse(hacks1_9_snapshot.useBelow(5)) + assertFalse(hacks1_9_snapshot.useBelow(15)) + assertFalse(hacks1_9_snapshot.useBelow(1000)) + + // Incompatible versions never get any hacks + val hacks2_5 = new Hacks("2.5") + assertFalse(hacks2_5.useBelow(0)) + assertFalse(hacks2_5.useBelow(1)) + assertFalse(hacks2_5.useBelow(5)) + assertFalse(hacks2_5.useBelow(15)) + assertFalse(hacks2_5.useBelow(1000)) + } +} diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 4713fe6bf8..af6f47f735 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -5,6 +5,9 @@ import com.typesafe.tools.mima.core.ProblemFilters._ object BinaryIncompatibilities { val IR = Seq( + // 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( From 38636cf028b0506835c5c0dea5b1ab31cefa7d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 25 Jan 2025 14:06:32 +0100 Subject: [PATCH 07/30] Bump the version to 1.19.0-SNAPSHOT for the upcoming changes. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0f03e6a638..3f7c4d501e 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.18.3-SNAPSHOT", + current = "1.19.0-SNAPSHOT", binaryEmitted = "1.18" ) From 1987d8726b40405147bad5b4f3ee5bf06452afd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 29 Dec 2024 20:49:17 +0100 Subject: [PATCH 08/30] Add a desugaring pass between the base linker and the optimizer. Previously, the emitters and the optimizer all had to perform the same desugaring for `LinkTimeProperty` nodes. Instead, we now perform the desugaring in a dedicated phase, after the base linker. The reachability analysis records whether each method needs desugaring or not. We mark those that do so that the desugaring pass knows what to process. Methods that do not require desugaring are not processed, and so incur no additional cost. No caching is performed in `Desugarer`. It processes so few methods that caching makes it (slightly) *slower*. The machinery is heavy. It definitely outweighs the benefits in terms of duplication for `LinkTimeProperty` alone. However, the same machinery will be used to desugar `NewLambda` nodes. This commit serves as a stepping stone in that direction. --- .../scala/org/scalajs/ir/Transformers.scala | 22 ++- .../scalajs/linker/analyzer/Analysis.scala | 4 + .../scalajs/linker/analyzer/Analyzer.scala | 11 +- .../org/scalajs/linker/analyzer/Infos.scala | 2 + .../backend/emitter/FunctionEmitter.scala | 10 +- .../backend/wasmemitter/DerivedClasses.scala | 1 + .../backend/wasmemitter/FunctionEmitter.scala | 9 +- .../linker/checker/CheckingPhase.scala | 1 + .../linker/checker/ClassDefChecker.scala | 2 + .../scalajs/linker/checker/FeatureSet.scala | 28 ++- .../scalajs/linker/checker/IRChecker.scala | 5 +- .../scalajs/linker/frontend/BaseLinker.scala | 32 ++-- .../scalajs/linker/frontend/Desugarer.scala | 159 ++++++++++++++++++ .../linker/frontend/LinkerFrontendImpl.scala | 13 +- .../frontend/optimizer/OptimizerCore.scala | 3 - .../scalajs/linker/standard/LinkedClass.scala | 44 ++++- .../standard/LinkedTopLevelExport.scala | 3 +- .../org/scalajs/linker/IRCheckerTest.scala | 50 +++++- project/BinaryIncompatibilities.scala | 3 + 19 files changed, 342 insertions(+), 60 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/frontend/Desugarer.scala 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 bbc0c3350b..a2edeaf797 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -234,14 +234,8 @@ object Transformers { case jsMethodDef: JSMethodDef => transformJSMethodDef(jsMethodDef) - case JSPropertyDef(flags, name, getterBody, setterArgAndBody) => - JSPropertyDef( - flags, - transform(name), - transformTreeOpt(getterBody), - setterArgAndBody.map { case (arg, body) => - (arg, transform(body)) - })(Unversioned)(jsMethodPropDef.pos) + case jsPropertyDef: JSPropertyDef => + transformJSPropertyDef(jsPropertyDef) } } @@ -251,6 +245,18 @@ object Transformers { jsMethodDef.optimizerHints, Unversioned)(jsMethodDef.pos) } + def transformJSPropertyDef(jsPropertyDef: JSPropertyDef): JSPropertyDef = { + val JSPropertyDef(flags, name, getterBody, setterArgAndBody) = jsPropertyDef + JSPropertyDef( + flags, + transform(name), + transformTreeOpt(getterBody), + setterArgAndBody.map { case (arg, body) => + (arg, transform(body)) + } + )(Unversioned)(jsPropertyDef.pos) + } + def transformJSConstructorBody(body: JSConstructorBody): JSConstructorBody = { implicit val pos = body.pos diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala index 0c1b0118e5..781fc30c48 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala @@ -84,6 +84,8 @@ object Analysis { def methodInfos( namespace: MemberNamespace): scala.collection.Map[MethodName, MethodInfo] + def anyJSMemberNeedsDesugaring: Boolean + def displayName: String = className.nameString } @@ -103,6 +105,7 @@ object Analysis { def instantiatedSubclasses: scala.collection.Seq[ClassInfo] def nonExistent: Boolean def syntheticKind: MethodSyntheticKind + def needsDesugaring: Boolean def displayName: String = methodName.displayName @@ -161,6 +164,7 @@ object Analysis { def owningClass: ClassName def staticDependencies: scala.collection.Set[ClassName] def externalDependencies: scala.collection.Set[String] + def needsDesugaring: Boolean } sealed trait Error { 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 9dc14c1574..e1faf2fa1a 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 @@ -686,6 +686,9 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, val publicMethodInfos: mutable.Map[MethodName, MethodInfo] = methodInfos(MemberNamespace.Public) + def anyJSMemberNeedsDesugaring: Boolean = + data.jsMethodProps.exists(info => (info.globalFlags & ReachabilityInfo.FlagNeedsDesugaring) != 0) + def lookupAbstractMethod(methodName: MethodName): MethodInfo = { val candidatesIterator = for { ancestor <- ancestors.iterator @@ -1285,6 +1288,9 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, def isDefaultBridge: Boolean = syntheticKind.isInstanceOf[MethodSyntheticKind.DefaultBridge] + def needsDesugaring: Boolean = + (data.globalFlags & ReachabilityInfo.FlagNeedsDesugaring) != 0 + /** Throws MatchError if `!isDefaultBridge`. */ def defaultBridgeTarget: ClassName = (syntheticKind: @unchecked) match { case MethodSyntheticKind.DefaultBridge(target) => target @@ -1367,6 +1373,9 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, def staticDependencies: scala.collection.Set[ClassName] = _staticDependencies.keySet def externalDependencies: scala.collection.Set[String] = _externalDependencies.keySet + def needsDesugaring: Boolean = + (data.reachability.globalFlags & ReachabilityInfo.FlagNeedsDesugaring) != 0 + def reach(): Unit = followReachabilityInfo(data.reachability, this)(FromExports) } @@ -1441,7 +1450,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, } } - val globalFlags = data.globalFlags + val globalFlags = data.globalFlags & ~ReachabilityInfo.FlagNeedsDesugaring if (globalFlags != 0) { if ((globalFlags & ReachabilityInfo.FlagAccessedClassClass) != 0) { 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 1458e107f4..c713d5799e 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 @@ -115,6 +115,7 @@ object Infos { final val FlagAccessedImportMeta = 1 << 2 final val FlagUsedExponentOperator = 1 << 3 final val FlagUsedClassSuperClass = 1 << 4 + final val FlagNeedsDesugaring = 1 << 5 } /** Things from a given class that are reached by one method. */ @@ -395,6 +396,7 @@ object Infos { setFlag(ReachabilityInfo.FlagUsedClassSuperClass) def addReferencedLinkTimeProperty(linkTimeProperty: LinkTimeProperty): this.type = { + setFlag(ReachabilityInfo.FlagNeedsDesugaring) linkTimeProperties.append((linkTimeProperty.name, linkTimeProperty.tpe)) this } 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 df5ed07b70..2252d71cd4 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 @@ -1260,9 +1260,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def test(tree: Tree): Boolean = tree match { // Atomic expressions - case _: Literal => true - case _: JSNewTarget => true - case _: LinkTimeProperty => true + case _: Literal => true + case _: JSNewTarget => true // Vars (side-effect free, pure if immutable) case VarRef(name) => @@ -2811,11 +2810,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case AsInstanceOf(expr, tpe) => extractWithGlobals(genAsInstanceOf(transformExprNoChar(expr), tpe)) - case prop: LinkTimeProperty => - transformExpr( - config.coreSpec.linkTimeProperties.transformLinkTimeProperty(prop), - preserveChar) - // Transients case Transient(Cast(expr, tpe)) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala index 1935e130fc..c65065dab5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala @@ -146,6 +146,7 @@ object DerivedClasses { staticDependencies = Set.empty, externalDependencies = Set.empty, dynamicDependencies = Set.empty, + desugaringRequirements = LinkedClass.DesugaringRequirements.Empty, clazz.version ) } 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 7ef7a87ac3..68e1ab881d 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 @@ -550,7 +550,6 @@ private class FunctionEmitter private ( case t: Match => genMatch(t, expectedType) case t: Debugger => VoidType // ignore case t: Skip => VoidType - case t: LinkTimeProperty => genLinkTimeProperty(t) // JavaScript expressions case t: JSNew => genJSNew(t) @@ -590,7 +589,7 @@ private class FunctionEmitter private ( // Transients (only generated by the optimizer) case t: Transient => genTransient(t) - case _: JSSuperConstructorCall => + case _:JSSuperConstructorCall | _:LinkTimeProperty => throw new AssertionError(s"Invalid tree: $tree") } @@ -2649,12 +2648,6 @@ private class FunctionEmitter private ( ClassType(boxClassName, nullable = false) } - private def genLinkTimeProperty(tree: LinkTimeProperty): Type = { - val lit = ctx.coreSpec.linkTimeProperties.transformLinkTimeProperty(tree) - genLiteral(lit, lit.tpe) - lit.tpe - } - private def genJSNew(tree: JSNew): Type = { val JSNew(ctor, args) = tree diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala index ff198744de..ac62d4d25e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/CheckingPhase.scala @@ -24,5 +24,6 @@ sealed abstract class CheckingPhase object CheckingPhase { case object Compiler extends CheckingPhase case object BaseLinker extends CheckingPhase + case object Desugarer extends CheckingPhase case object Optimizer extends CheckingPhase } 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 b3dd10acb8..671a320632 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 @@ -855,6 +855,8 @@ private final class ClassDefChecker(classDef: ClassDef, } case LinkTimeProperty(name) => + if (!featureSet.supports(FeatureSet.LinkTimeProperty)) + reportError(i"Illegal link-time property '$name' after desugaring") // JavaScript expressions 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 73f89b0d8b..59f1d89c54 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,17 +36,20 @@ private[checker] object FeatureSet { // Individual features + /** The `LinkTimeProperty` IR node. */ + val LinkTimeProperty = new FeatureSet(1 << 0) + /** Optional constructors in module classes and JS classes. */ - val OptionalConstructors = new FeatureSet(1 << 0) + val OptionalConstructors = new FeatureSet(1 << 1) /** Explicit reflective proxy definitions. */ - val ReflectiveProxies = new FeatureSet(1 << 1) + val ReflectiveProxies = new FeatureSet(1 << 2) /** Transients that are the result of optimizations. */ - val OptimizedTransients = new FeatureSet(1 << 2) + val OptimizedTransients = new FeatureSet(1 << 3) /** Records and record types. */ - val Records = new FeatureSet(1 << 3) + val Records = new FeatureSet(1 << 4) /** Relaxed constructor discipline. * @@ -55,7 +58,7 @@ private[checker] object FeatureSet { * - `this.x = ...` assignments before the delegate call can assign super class fields. * - `StoreModule` can be anywhere, or not be there at all. */ - val RelaxedCtorBodies = new FeatureSet(1 << 4) + val RelaxedCtorBodies = new FeatureSet(1 << 5) // Common sets @@ -63,14 +66,23 @@ private[checker] object FeatureSet { private val Linked = OptionalConstructors | ReflectiveProxies + /** Features that must be desugared away. */ + private val NeedsDesugaring = + LinkTimeProperty + + /** IR that is only the result of desugaring (currently empty). */ + private val Desugared = + Empty + /** IR that is only the result of optimizations. */ private val Optimized = OptimizedTransients | Records | RelaxedCtorBodies /** The set of features allowed as output of the given phase. */ def allowedAfter(phase: CheckingPhase): FeatureSet = phase match { - case Compiler => Empty - case BaseLinker => Linked - case Optimizer => Linked | Optimized + case Compiler => NeedsDesugaring + case BaseLinker => Linked | NeedsDesugaring + case Desugarer => Linked | Desugared + case Optimizer => Linked | Desugared | Optimized } } 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 de827f396a..a6c786fb01 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 @@ -577,7 +577,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheckAny(expr, env) checkIsAsInstanceTargetType(tpe) - case LinkTimeProperty(name) => + case LinkTimeProperty(name) if featureSet.supports(FeatureSet.LinkTimeProperty) => // JavaScript expressions @@ -760,7 +760,8 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheck(elem, env) } - case _:RecordSelect | _:RecordValue | _:Transient | _:JSSuperConstructorCall => + case _:RecordSelect | _:RecordValue | _:Transient | + _:JSSuperConstructorCall | _:LinkTimeProperty => reportError("invalid tree") } } 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 ddf5b71f14..07eb08b294 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 @@ -129,17 +129,20 @@ private[frontend] object BaseLinker { classInfo.isAnySubclassInstantiated } - val methods = classDef.methods.filter { m => - val methodInfo = - classInfo.methodInfos(m.flags.namespace)(m.methodName) - - val reachable = methodInfo.isReachable - assert(m.body.isDefined || !reachable, - s"The abstract method ${classDef.name.name}.${m.methodName} " + - "is reachable.") - - reachable - } + // Will stay empty for most classes + var desugaringRequirements = LinkedClass.DesugaringRequirements.Empty + + val methods: List[MethodDef] = classDef.methods.iterator + .map(m => m -> classInfo.methodInfos(m.flags.namespace)(m.methodName)) + .filter(_._2.isReachable) + .map { case (m, info) => + assert(m.body.isDefined, + s"The abstract method ${classDef.name.name}.${m.methodName} is reachable.") + if (info.needsDesugaring) + desugaringRequirements = desugaringRequirements.addMethod(m.flags.namespace, m.methodName) + m + } + .toList val jsConstructor = if (classInfo.isAnySubclassInstantiated) classDef.jsConstructor @@ -149,6 +152,9 @@ private[frontend] object BaseLinker { if (classInfo.isAnySubclassInstantiated) classDef.jsMethodProps else Nil + if (classInfo.anyJSMemberNeedsDesugaring) + desugaringRequirements = desugaringRequirements.addAnyExportedMember() + val jsNativeMembers = classDef.jsNativeMembers .filter(m => classInfo.jsNativeMembersUsed.contains(m.name.name)) @@ -181,6 +187,7 @@ private[frontend] object BaseLinker { staticDependencies = classInfo.staticDependencies.toSet, externalDependencies = classInfo.externalDependencies.toSet, dynamicDependencies = classInfo.dynamicDependencies.toSet, + desugaringRequirements, version) val linkedTopLevelExports = for { @@ -189,7 +196,8 @@ private[frontend] object BaseLinker { val infos = analysis.topLevelExportInfos( (ModuleID(topLevelExport.moduleID), topLevelExport.topLevelExportName)) new LinkedTopLevelExport(classDef.className, topLevelExport, - infos.staticDependencies.toSet, infos.externalDependencies.toSet) + infos.staticDependencies.toSet, infos.externalDependencies.toSet, + needsDesugaring = infos.needsDesugaring) } (linkedClass, linkedTopLevelExports) 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 new file mode 100644 index 0000000000..65323bfd69 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Desugarer.scala @@ -0,0 +1,159 @@ +/* + * 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.logging._ + +import org.scalajs.linker.standard._ +import org.scalajs.linker.checker._ + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Transformers._ +import org.scalajs.ir.Trees._ +import org.scalajs.ir.{Position, Version} + +/** Desugars a linking unit. */ +final class Desugarer(config: CommonPhaseConfig, checkIR: Boolean) { + import Desugarer._ + + private val desugarTransformer = new DesugarTransformer(config.coreSpec) + + def desugar(unit: LinkingUnit, logger: Logger): LinkingUnit = { + val result = logger.time("Desugarer: Desugar") { + val desugaredClasses = unit.classDefs.map(desugarClass(_)) + val desugaredTopLevelExports = unit.topLevelExports.map(desugarTopLevelExport(_)) + + new LinkingUnit(desugaredClasses, desugaredTopLevelExports, + unit.moduleInitializers, unit.globalInfo) + } + + if (checkIR) { + logger.time("Desugarer: Check IR") { + val errorCount = IRChecker.check(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)") + } + } + } + + result + } + + private def desugarClass(linkedClass: LinkedClass): LinkedClass = { + import linkedClass._ + + if (desugaringRequirements.isEmpty) { + linkedClass + } else { + val newMethods = methods.map { method => + if (!desugaringRequirements.containsMethod(method.flags.namespace, method.methodName)) + method + else + desugarTransformer.transformMethodDef(method) + } + + val newJSConstructorDef = + if (!desugaringRequirements.exportedMembers) jsConstructorDef + else jsConstructorDef.map(desugarTransformer.transformJSConstructorDef(_)) + + val newExportedMembers = + if (!desugaringRequirements.exportedMembers) exportedMembers + else exportedMembers.map(desugarTransformer.transformJSMethodPropDef(_)) + + new LinkedClass( + name, + kind, + jsClassCaptures, + superClass, + interfaces, + jsSuperClass, + jsNativeLoadSpec, + fields, + methods = newMethods, + jsConstructorDef = newJSConstructorDef, + exportedMembers = newExportedMembers, + jsNativeMembers, + optimizerHints, + pos, + ancestors, + hasInstances, + hasDirectInstances, + hasInstanceTests, + hasRuntimeTypeInfo, + fieldsRead, + staticFieldsRead, + staticDependencies, + externalDependencies, + dynamicDependencies, + LinkedClass.DesugaringRequirements.Empty, + version + ) + } + } + + private def desugarTopLevelExport(tle: LinkedTopLevelExport): LinkedTopLevelExport = { + import tle._ + if (!tle.needsDesugaring) { + tle + } else { + val newTree = desugarTransformer.transformTopLevelExportDef(tree) + new LinkedTopLevelExport(owningClass, newTree, staticDependencies, + externalDependencies, needsDesugaring = false) + } + } +} + +private[linker] object Desugarer { + + private final class DesugarTransformer(coreSpec: CoreSpec) + extends ClassTransformer { + + override def transform(tree: Tree): Tree = { + tree match { + case prop: LinkTimeProperty => + coreSpec.linkTimeProperties.transformLinkTimeProperty(prop) + + case _ => + super.transform(tree) + } + } + + /* Transfer Version from old members to transformed members. + * We can do this because the transformation only depends on the + * `coreSpec`, which is immutable. + */ + + override def transformMethodDef(methodDef: MethodDef): MethodDef = { + val newMethodDef = super.transformMethodDef(methodDef) + newMethodDef.copy()(newMethodDef.optimizerHints, methodDef.version)(newMethodDef.pos) + } + + override def transformJSConstructorDef(jsConstructor: JSConstructorDef): JSConstructorDef = { + val newJSConstructor = super.transformJSConstructorDef(jsConstructor) + newJSConstructor.copy()(newJSConstructor.optimizerHints, jsConstructor.version)( + newJSConstructor.pos) + } + + override def transformJSMethodDef(jsMethodDef: JSMethodDef): JSMethodDef = { + val newJSMethodDef = super.transformJSMethodDef(jsMethodDef) + newJSMethodDef.copy()(newJSMethodDef.optimizerHints, jsMethodDef.version)( + newJSMethodDef.pos) + } + + override def transformJSPropertyDef(jsPropertyDef: JSPropertyDef): JSPropertyDef = { + val newJSPropertyDef = super.transformJSPropertyDef(jsPropertyDef) + newJSPropertyDef.copy()(jsPropertyDef.version)(newJSPropertyDef.pos) + } + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkerFrontendImpl.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkerFrontendImpl.scala index 11d10064a9..d75fd57eab 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkerFrontendImpl.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkerFrontendImpl.scala @@ -40,6 +40,9 @@ final class LinkerFrontendImpl private (config: LinkerFrontendImpl.Config) private[this] val linker: BaseLinker = new BaseLinker(config.commonConfig, config.checkIR) + private[this] val desugarer: Desugarer = + new Desugarer(config.commonConfig, config.checkIR) + private[this] val optOptimizer: Option[IncOptimizer] = LinkerFrontendImplPlatform.createOptimizer(config) @@ -69,8 +72,14 @@ final class LinkerFrontendImpl private (config: LinkerFrontendImpl.Config) preOptimizerRequirements) } - val optimizedResult = optOptimizer.fold(linkResult) { optimizer => - linkResult.flatMap(optimize(_, symbolRequirements, optimizer, logger)) + val desugaredResult = linkResult.map { unit => + logger.time("Desugarer") { + desugarer.desugar(unit, logger) + } + } + + val optimizedResult = optOptimizer.fold(desugaredResult) { optimizer => + desugaredResult.flatMap(optimize(_, symbolRequirements, optimizer, logger)) } optimizedResult.map { unit => 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 dae5f87e43..9c026ac032 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 @@ -589,9 +589,6 @@ private[optimizer] abstract class OptimizerCore( } } - case prop: LinkTimeProperty => - config.coreSpec.linkTimeProperties.transformLinkTimeProperty(prop) - // JavaScript expressions case JSNew(ctor, args) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala index afa257b289..010214777a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala @@ -14,7 +14,7 @@ package org.scalajs.linker.standard import org.scalajs.ir.Trees._ import org.scalajs.ir.{ClassKind, Position, Version} -import org.scalajs.ir.Names.{ClassName, FieldName} +import org.scalajs.ir.Names.{ClassName, FieldName, MethodName} /** A ClassDef after linking. * @@ -65,6 +65,9 @@ final class LinkedClass( val externalDependencies: Set[String], val dynamicDependencies: Set[ClassName], + // Desugaring requirements + val desugaringRequirements: LinkedClass.DesugaringRequirements, + val version: Version) { require(ancestors.headOption.contains(name.name), @@ -89,3 +92,42 @@ final class LinkedClass( def fullName: String = className.nameString } + +object LinkedClass { + /** Desugaring requirements of a `LinkedClass`. + * + * These requirements are a set of members that need desugaring. + */ + final class DesugaringRequirements private ( + methods: Vector[Set[MethodName]], // indexed by MemberNamespace ordinal + val exportedMembers: Boolean + ) { + private def this() = { + this( + methods = Vector.fill(MemberNamespace.Count)(Set.empty), + exportedMembers = false + ) + } + + /** Are these requirements empty, i.e., does the corresponding require no desugaring at all? */ + def isEmpty: Boolean = + this eq DesugaringRequirements.Empty // by construction, only that specific instance is empty + + /** Do the requirements contain the given method, i.e., does that method need desugaring? */ + def containsMethod(namespace: MemberNamespace, methodName: MethodName): Boolean = + methods(namespace.ordinal).contains(methodName) + + def addMethod(namespace: MemberNamespace, methodName: MethodName): DesugaringRequirements = { + val newMethods = + methods.updated(namespace.ordinal, methods(namespace.ordinal) + methodName) + new DesugaringRequirements(newMethods, exportedMembers) + } + + def addAnyExportedMember(): DesugaringRequirements = + new DesugaringRequirements(methods, exportedMembers = true) + } + + object DesugaringRequirements { + val Empty: DesugaringRequirements = new DesugaringRequirements() + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedTopLevelExport.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedTopLevelExport.scala index 1702ecd02e..339280f150 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedTopLevelExport.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedTopLevelExport.scala @@ -19,7 +19,8 @@ final class LinkedTopLevelExport( val owningClass: ClassName, val tree: TopLevelExportDef, val staticDependencies: Set[ClassName], - val externalDependencies: Set[String] + val externalDependencies: Set[String], + val needsDesugaring: Boolean ) { def exportName: String = tree.topLevelExportName } 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 d283ae189e..1d6dd4759f 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala @@ -18,8 +18,9 @@ import scala.util.{Failure, Success} import org.junit.Test import org.junit.Assert._ -import org.scalajs.ir.ClassKind +import org.scalajs.ir.{ClassKind, EntryPointsInfo} import org.scalajs.ir.Names._ +import org.scalajs.ir.Transformers._ import org.scalajs.ir.Trees._ import org.scalajs.ir.Types._ @@ -427,6 +428,41 @@ class IRCheckerTest { } object IRCheckerTest { + /** Version of the minilib where we have replaced every node requiring + * desugaring by a placeholder. + * + * We need this to directly feed to the IR checker post-optimizer, since + * nodes requiring desugaring are rejected at that point. + */ + private lazy val minilibRequiringNoDesugaring: Future[Seq[IRFile]] = { + import scala.concurrent.ExecutionContext.Implicits.global + + TestIRRepo.minilib.map { stdLibFiles => + for (irFile <- stdLibFiles) yield { + val irFileImpl = IRFileImpl.fromIRFile(irFile) + + val patchedTreeFuture = irFileImpl.tree.map { tree => + new ClassTransformer { + override def transform(tree: Tree): Tree = tree match { + case tree: LinkTimeProperty => zeroOf(tree.tpe) + case _ => super.transform(tree) + } + }.transformClassDef(tree) + } + + new IRFileImpl(irFileImpl.path, irFileImpl.version) { + /** Entry points information for this file. */ + def entryPointsInfo(implicit ec: ExecutionContext): Future[EntryPointsInfo] = + irFileImpl.entryPointsInfo(ec) + + /** IR Tree of this file. */ + def tree(implicit ec: ExecutionContext): Future[ClassDef] = + patchedTreeFuture + } + } + } + } + def testLinkNoIRError(classDefs: Seq[ClassDef], moduleInitializers: List[ModuleInitializer], postOptimizer: Boolean = false)( @@ -467,8 +503,8 @@ object IRCheckerTest { .factory("IRCheckerTest") .none() - TestIRRepo.minilib.flatMap { stdLibFiles => - if (postOptimizer) { + if (postOptimizer) { + minilibRequiringNoDesugaring.flatMap { stdLibFiles => val refiner = new Refiner(CommonPhaseConfig.fromStandardConfig(config), checkIR = true) Future.traverse(stdLibFiles)(f => IRFileImpl.fromIRFile(f).tree).flatMap { stdLibClassDefs => @@ -480,7 +516,9 @@ object IRCheckerTest { refiner.refine(allClassDefs.map(c => (c, UNV)), moduleInitializers, noSymbolRequirements, logger) } - } else { + }.map(_ => ()) + } else { + TestIRRepo.minilib.flatMap { stdLibFiles => val linkerFrontend = StandardLinkerFrontend(config) val irFiles = ( stdLibFiles ++ @@ -488,7 +526,7 @@ object IRCheckerTest { PrivateLibHolder.files ) linkerFrontend.link(irFiles, moduleInitializers, noSymbolRequirements, logger) - } - }.map(_ => ()) + }.map(_ => ()) + } } } diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index af6f47f735..8ac39f199d 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -11,6 +11,9 @@ object BinaryIncompatibilities { ) 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( From b38201c0cf6f99fc146d0ee90de989b23d6e9c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 16 Jan 2025 23:35:37 +0100 Subject: [PATCH 09/30] Drop support for non-strict floats. Non-strict floats were deprecated 3 years ago in Scala.js 1.9.0. I don't recall seeing any comment about them ever since. --- Jenkinsfile | 36 -- javalib/src/main/scala/java/lang/Float.scala | 9 +- .../scala/java/lang/FloatingPointBits.scala | 46 +-- .../scalajs/linker/interface/Semantics.scala | 25 +- .../linker/interface/StandardConfig.scala | 1 - .../backend/WebAssemblyLinkerBackend.scala | 4 - .../linker/backend/emitter/CoreJSLib.scala | 312 +++++++++--------- .../linker/backend/emitter/SJSGen.scala | 16 +- project/BinaryIncompatibilities.scala | 2 + project/Build.scala | 1 - .../scalajs/testsuite/utils/BuildInfo.scala | 1 - .../scalajs/testsuite/utils/Platform.scala | 34 -- .../testsuite/compiler/FloatJSTest.scala | 2 - .../scalajs/testsuite/utils/Requires.scala | 5 - .../scalajs/testsuite/utils/Platform.scala | 2 - .../testsuite/compiler/DoubleTest.scala | 4 - .../testsuite/compiler/FloatTest.scala | 28 +- .../scalajs/testsuite/compiler/LongTest.scala | 2 - .../compiler/RuntimeTypeTestsTest.scala | 2 +- .../testsuite/javalib/lang/ClassTest.scala | 2 +- .../testsuite/javalib/lang/FloatTest.scala | 15 +- .../testsuite/javalib/lang/ObjectTest.scala | 2 +- .../javalib/math/BigDecimalConvertTest.scala | 14 +- .../javalib/math/BigIntegerConvertTest.scala | 12 - 24 files changed, 196 insertions(+), 381 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8f6ecc3c8e..5cbf59ce7f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -232,17 +232,6 @@ def Tasks = [ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ $testSuite$v/test && - sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - $testSuite$v/test && - sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSStage in Global := FullOptStage' \ - $testSuite$v/test && - sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - $testSuite$v/test && sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ $testSuite$v/test && @@ -312,20 +301,6 @@ def Tasks = [ 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ ++$scala $testSuite$v/test ''', @@ -355,17 +330,6 @@ def Tasks = [ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion).withAllowBigIntsForLongs(true)))' \ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion).withAllowBigIntsForLongs(true)).withOptimizer(false))' \ diff --git a/javalib/src/main/scala/java/lang/Float.scala b/javalib/src/main/scala/java/lang/Float.scala index c9c6ea2e84..8fa4ce3070 100644 --- a/javalib/src/main/scala/java/lang/Float.scala +++ b/javalib/src/main/scala/java/lang/Float.scala @@ -150,14 +150,7 @@ object Float { val zDouble = z.toDouble if (zDouble == z0) { - /* This branch is always taken when strictFloats are disabled, and there - * is no Math.fround support. In that case, Floats are basically - * equivalent to Doubles, and we make no specific guarantee about the - * result, so we can quickly return `z`. - * More importantly, the computations in the `else` branch assume that - * Float operations are exact, so we must return early. - * - * This branch is also always taken when z0 is 0.0 or Infinity, which the + /* This branch is always taken when z0 is 0.0 or Infinity, which the * `else` branch assumes does not happen. */ z diff --git a/javalib/src/main/scala/java/lang/FloatingPointBits.scala b/javalib/src/main/scala/java/lang/FloatingPointBits.scala index fb9b89ff93..96e1c8f64c 100644 --- a/javalib/src/main/scala/java/lang/FloatingPointBits.scala +++ b/javalib/src/main/scala/java/lang/FloatingPointBits.scala @@ -149,7 +149,7 @@ private[lang] object FloatingPointBits { float32Array(0) = value int32Array(0) } else { - floatToIntBitsPolyfill(value.toDouble) + floatToIntBitsPolyfill(value) } } @@ -181,8 +181,7 @@ private[lang] object FloatingPointBits { * 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, which is also more - * predictable, since the results do not depend on strict floats semantics. + * We therefore do all computations in Doubles here. */ private def intBitsToFloatPolyfill(bits: Int): scala.Double = { @@ -194,21 +193,23 @@ private[lang] object FloatingPointBits { decodeIEEE754(ebits, fbits, floatPowsOf2, scala.Float.MinPositiveValue, sign, e, f) } - private def floatToIntBitsPolyfill(value: scala.Double): Int = { + 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 avr = forceFround(av) val powsOf2 = this.floatPowsOf2 // local cache - val e = encodeIEEE754Exponent(ebits, powsOf2, avr) - val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, scala.Float.MinPositiveValue.toDouble, avr, e) + val e = encodeIEEE754Exponent(ebits, powsOf2, av) + val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, scala.Float.MinPositiveValue.toDouble, av, e) // Encode s | (e << fbits) | rawToInt(f) @@ -277,37 +278,6 @@ private[lang] object FloatingPointBits { } } - /** Force rounding of `av` to fit in 32 bits (this is a manual `fround`). - * - * `av` must not be negative, i.e., `av < 0.0` must be false (it can be - * `NaN` or `Infinity`). - * - * When we use strict-float semantics, this is redundant, because the input - * came from a `Float` and is therefore guaranteed to be rounded already. - * However, here we don't know whether we use strict floats semantics or - * not, so we must always do it. This is not a big deal because, if this - * code is called, then any operation on `Float`s is calling the same code - * from the `CoreJSLib`, so doing one more such operation for - * `floatToIntBits` is negligible. - * - * TODO Remove this when we get rid of non-strict float semantics altogether. - */ - @inline - private def forceFround(av: scala.Double): scala.Double = { - // See the `fround` polyfill in CoreJSLib - val overflowThreshold = 3.4028235677973366e38 - val normalThreshold = 1.1754943508222875e-38 - if (av >= overflowThreshold) { - scala.Double.PositiveInfinity - } else if (av >= normalThreshold) { - val p = av * 536870913.0 // pow(2, 29) + 1 - p + (av - p) - } else { - val roundingFactor = scala.Double.MinPositiveValue / scala.Float.MinPositiveValue.toDouble - (av * roundingFactor) / roundingFactor - } - } - private def encodeIEEE754Exponent(ebits: Int, powsOf2: js.Array[scala.Double], av: scala.Double): Int = { diff --git a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala index 5795b1c5db..d8608016c1 100644 --- a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala +++ b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala @@ -12,6 +12,8 @@ package org.scalajs.linker.interface +import scala.annotation.compileTimeOnly + import CheckedBehavior._ import Fingerprint.FingerprintBuilder @@ -23,12 +25,16 @@ final class Semantics private ( val nullPointers: CheckedBehavior, val stringIndexOutOfBounds: CheckedBehavior, val moduleInit: CheckedBehavior, - val strictFloats: Boolean, val productionMode: Boolean, val runtimeClassNameMapper: Semantics.RuntimeClassNameMapper) { import Semantics._ + @deprecated( + "non-strict floats are not supported anymore; strictFloats is always true", + since = "1.19.0") + val strictFloats: Boolean = true + def withAsInstanceOfs(behavior: CheckedBehavior): Semantics = copy(asInstanceOfs = behavior) @@ -50,13 +56,11 @@ final class Semantics private ( def withModuleInit(moduleInit: CheckedBehavior): Semantics = copy(moduleInit = moduleInit) - @deprecated( - "Scala.js now uses strict floats by default. " + - "Non-strict float semantics are deprecated and will eventually be " + - "removed.", - "1.9.0") + @compileTimeOnly( + "Non-strict floats are not supported anymore. " + + "The default is `true` and cannot be turned to `false`.") def withStrictFloats(strictFloats: Boolean): Semantics = - copy(strictFloats = strictFloats) + this def withProductionMode(productionMode: Boolean): Semantics = copy(productionMode = productionMode) @@ -86,7 +90,6 @@ final class Semantics private ( this.nullPointers == that.nullPointers && this.stringIndexOutOfBounds == that.stringIndexOutOfBounds && this.moduleInit == that.moduleInit && - this.strictFloats == that.strictFloats && this.productionMode == that.productionMode && this.runtimeClassNameMapper == that.runtimeClassNameMapper case _ => @@ -103,7 +106,6 @@ final class Semantics private ( acc = mix(acc, nullPointers.##) acc = mix(acc, stringIndexOutOfBounds.##) acc = mix(acc, moduleInit.##) - acc = mix(acc, strictFloats.##) acc = mix(acc, productionMode.##) acc = mixLast(acc, runtimeClassNameMapper.##) finalizeHash(acc, 10) @@ -118,7 +120,6 @@ final class Semantics private ( | nullPointers = $nullPointers, | stringIndexOutOfBounds = $stringIndexOutOfBounds, | moduleInit = $moduleInit, - | strictFloats = $strictFloats, | productionMode = $productionMode |)""".stripMargin } @@ -131,7 +132,6 @@ final class Semantics private ( nullPointers: CheckedBehavior = this.nullPointers, stringIndexOutOfBounds: CheckedBehavior = this.stringIndexOutOfBounds, moduleInit: CheckedBehavior = this.moduleInit, - strictFloats: Boolean = this.strictFloats, productionMode: Boolean = this.productionMode, runtimeClassNameMapper: RuntimeClassNameMapper = this.runtimeClassNameMapper): Semantics = { @@ -143,7 +143,6 @@ final class Semantics private ( nullPointers = nullPointers, stringIndexOutOfBounds = stringIndexOutOfBounds, moduleInit = moduleInit, - strictFloats = strictFloats, productionMode = productionMode, runtimeClassNameMapper = runtimeClassNameMapper) } @@ -262,7 +261,6 @@ object Semantics { .addField("nullPointers", semantics.nullPointers) .addField("stringIndexOutOfBounds", semantics.stringIndexOutOfBounds) .addField("moduleInit", semantics.moduleInit) - .addField("strictFloats", semantics.strictFloats) .addField("productionMode", semantics.productionMode) .addField("runtimeClassNameMapper", semantics.runtimeClassNameMapper) .build() @@ -277,7 +275,6 @@ object Semantics { nullPointers = Fatal, stringIndexOutOfBounds = Fatal, moduleInit = Unchecked, - strictFloats = true, productionMode = false, runtimeClassNameMapper = RuntimeClassNameMapper.keepAll()) } diff --git a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala index 508728fce5..2f3a2afe7b 100644 --- a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala +++ b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala @@ -192,7 +192,6 @@ final class StandardConfig private ( * * - `moduleKind == ModuleKind.ESModule` * - `esFeatures.useECMAScript2015Semantics == true` (true by default) - * - `semantics.strictFloats == true` (true by default; non-strict floats are deprecated) * * We may lift these restrictions in the future, although we do not expect * to do so. diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index ba12a26b59..a167a7bf9f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -40,10 +40,6 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) coreSpec.esFeatures.useECMAScript2015Semantics, s"The WebAssembly backend only supports the ECMAScript 2015 semantics." ) - require( - coreSpec.semantics.strictFloats, - "The WebAssembly backend only supports strict float semantics." - ) require(coreSpec.targetIsWebAssembly, s"A WebAssembly backend cannot be used with CoreSpec targeting JavaScript") 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 467e73d68b..8b30a408f5 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 @@ -191,154 +191,150 @@ private[emitter] object CoreJSLib { case FroundBuiltin => val v = varRef("v") - if (!strictFloats) { - genArrowFunction(paramList(v), Return(+v)) - } else { - val Float32ArrayRef = globalRef("Float32Array") - - /* (function(array) { - * return function(v) { - * array[0] = v; - * return array[0]; - * } - * })(new Float32Array(1)) - * - * Allocating the Float32Array once and for all, and capturing it - * in an IIFE, is *much* faster than recreating it in every call of - * the polyfill (about an order of magnitude). - */ - val array = varRef("array") - val typedArrayPolyfillInner = genArrowFunction(paramList(v), { - Block( - BracketSelect(array, 0) := v, - Return(BracketSelect(array, 0)) - ) - }) - val typedArrayPolyfill = Apply( - genArrowFunction(paramList(array), Return(typedArrayPolyfillInner)), - New(Float32ArrayRef, 1 :: Nil) :: Nil) - - // scalastyle:off line.size.limit - /* Originally inspired by the Typed Array polyfills written by - * Joshua Bell: - * https://github.com/inexorabletash/polyfill/blob/a682f42c1092280bb01907c245979fb07219513d/typedarray.js#L150-L255 - * Then simplified quite a lot because - * 1) we do not need to produce the actual bit string that serves - * as storage of the floats, and - * 2) we are only interested in the float32 case. - * - * Eventually, the last bits of the above were replaced by an - * application of Veltkamp's splitting (see below). The inspiration - * for that use case came from core-js' implementation at - * https://github.com/zloirock/core-js/blob/a3f591658e063a6e2c2594ec3c80eff16340a98d/packages/core-js/internals/math-fround.js - * The code does not mention Veltkamp's splitting, but the PR - * discussion that led to it does, although with a question mark, - * and without any explanation of how/why it works: - * https://github.com/paulmillr/es6-shim/pull/140#issuecomment-91787165 - * We tracked down the descriptions and proofs relative to - * Veltkamp's splitting and re-derived an implementation from there. - * - * The direct tests for this polyfill are the tests for `toFloat` - * in org.scalajs.testsuite.compiler.DoubleTest. - */ - // scalastyle:on line.size.limit - val sign = varRef("sign") - val av = varRef("av") - val p = varRef("p") - - val Inf = double(Double.PositiveInfinity) - val overflowThreshold = double(3.4028235677973366e38) - val normalThreshold = double(1.1754943508222875e-38) - - val noTypedArrayPolyfill = genArrowFunction(paramList(v), Block( - v := +v, // turns `null` into +0, making sure not to deoptimize what follows - const(sign, If(v < 0, -1, 1)), // 1 for NaN, +0 and -0 - const(av, sign * v), // abs(v), or -0 if v is -0 - If(av >= overflowThreshold, { // also handles the case av === Infinity - Return(sign * Inf) - }, If(av >= normalThreshold, Block( - /* Here, we know that both the input and output are expressed - * in a Double normal form, so standard floating point - * algorithms from papers can be used. - * - * We use Veltkamp's splitting, as described and studied in - * Sylvie Boldo. - * Pitfalls of a Full Floating-Point Proof: Example on the - * Formal Proof of the Veltkamp/Dekker Algorithms - * https://dx.doi.org/10.1007/11814771_6 - * Section 3, with β = 2, t = 53, s = 53 - 24 = 29, x = av. - * 53 is the number of effective mantissa bits in a Double; - * 24 in a Float. - * - * ◦ is the round-to-nearest operation with a tie-breaking - * rule (in our case, break-to-even). - * - * Let C = βˢ + 1 = 536870913 - * p = ◦(x × C) - * q = ◦(x − p) - * x₁ = ◦(p + q) - * - * Boldo proves that x₁ is the (t-s)-bit float closest to x, - * using the same tie-breaking rule as ◦. Since (t-s) = 24, - * this is the closest float32 (with 24 mantissa bits), and - * therefore the correct result of `fround`. - * - * Boldo also proves that if the computation of x × C does not - * cause overflow, then none of the following operations will - * cause overflow. We know that x (av) is less than the - * overflowThreshold, and overflowThreshold × C does not - * overflow, so that computation can never cause an overflow. - * - * If the reader does not have access to Boldo's paper, they - * may refer instead to - * Claude-Pierre Jeannerod, Jean-Michel Muller, Paul Zimmermann. - * On various ways to split a floating-point number. - * ARITH 2018 - 25th IEEE Symposium on Computer Arithmetic, - * Jun 2018, Amherst (MA), United States. - * pp.53-60, 10.1109/ARITH.2018.8464793. hal-01774587v2 - * available at - * https://hal.inria.fr/hal-01774587v2/document - * Section III, although that paper defers some theorems and - * proofs to Boldo's. - */ - const(p, av * 536870913), - Return(sign * (p + (av - p))) - ), { - /* Here, the result is represented as a subnormal form in a - * float32 representation. - * - * We round `av` to the nearest multiple of the smallest - * positive Float value (i.e., `Float.MinPositiveValue`), - * breaking ties to an even multiple. - * - * We do this by leveraging the inherent loss of precision near - * the minimum positive *double* value: conceptually, we divide - * the value by - * Float.MinPositiveValue / Double.MinPositiveValue - * which will drop the excess precision, applying exactly the - * rounding strategy that we want. Then we multiply the value - * back by the same constant. - * - * However, `Float.MinPositiveValue / Double.MinPositiveValue` - * is not representable as a finite Double. Therefore, we - * instead use the *inverse* constant - * Double.MinPositiveValue / Float.MinPositiveValue - * and we first multiply by that constant, then divide by it. - * - * --- - * - * As an additional "hack", the input values NaN, +0 and -0 - * also fall in this code path. For them, this computation - * happens to be an identity, and is therefore correct as well. - */ - val roundingFactor = double(Double.MinPositiveValue / Float.MinPositiveValue.toDouble) - Return(sign * ((av * roundingFactor) / roundingFactor)) - })) - )) + val Float32ArrayRef = globalRef("Float32Array") - If(typeof(Float32ArrayRef) !== str("undefined"), - typedArrayPolyfill, noTypedArrayPolyfill) - } + /* (function(array) { + * return function(v) { + * array[0] = v; + * return array[0]; + * } + * })(new Float32Array(1)) + * + * Allocating the Float32Array once and for all, and capturing it + * in an IIFE, is *much* faster than recreating it in every call of + * the polyfill (about an order of magnitude). + */ + val array = varRef("array") + val typedArrayPolyfillInner = genArrowFunction(paramList(v), { + Block( + BracketSelect(array, 0) := v, + Return(BracketSelect(array, 0)) + ) + }) + val typedArrayPolyfill = Apply( + genArrowFunction(paramList(array), Return(typedArrayPolyfillInner)), + New(Float32ArrayRef, 1 :: Nil) :: Nil) + + // scalastyle:off line.size.limit + /* Originally inspired by the Typed Array polyfills written by + * Joshua Bell: + * https://github.com/inexorabletash/polyfill/blob/a682f42c1092280bb01907c245979fb07219513d/typedarray.js#L150-L255 + * Then simplified quite a lot because + * 1) we do not need to produce the actual bit string that serves + * as storage of the floats, and + * 2) we are only interested in the float32 case. + * + * Eventually, the last bits of the above were replaced by an + * application of Veltkamp's splitting (see below). The inspiration + * for that use case came from core-js' implementation at + * https://github.com/zloirock/core-js/blob/a3f591658e063a6e2c2594ec3c80eff16340a98d/packages/core-js/internals/math-fround.js + * The code does not mention Veltkamp's splitting, but the PR + * discussion that led to it does, although with a question mark, + * and without any explanation of how/why it works: + * https://github.com/paulmillr/es6-shim/pull/140#issuecomment-91787165 + * We tracked down the descriptions and proofs relative to + * Veltkamp's splitting and re-derived an implementation from there. + * + * The direct tests for this polyfill are the tests for `toFloat` + * in org.scalajs.testsuite.compiler.DoubleTest. + */ + // scalastyle:on line.size.limit + val sign = varRef("sign") + val av = varRef("av") + val p = varRef("p") + + val Inf = double(Double.PositiveInfinity) + val overflowThreshold = double(3.4028235677973366e38) + val normalThreshold = double(1.1754943508222875e-38) + + val noTypedArrayPolyfill = genArrowFunction(paramList(v), Block( + v := +v, // turns `null` into +0, making sure not to deoptimize what follows + const(sign, If(v < 0, -1, 1)), // 1 for NaN, +0 and -0 + const(av, sign * v), // abs(v), or -0 if v is -0 + If(av >= overflowThreshold, { // also handles the case av === Infinity + Return(sign * Inf) + }, If(av >= normalThreshold, Block( + /* Here, we know that both the input and output are expressed + * in a Double normal form, so standard floating point + * algorithms from papers can be used. + * + * We use Veltkamp's splitting, as described and studied in + * Sylvie Boldo. + * Pitfalls of a Full Floating-Point Proof: Example on the + * Formal Proof of the Veltkamp/Dekker Algorithms + * https://dx.doi.org/10.1007/11814771_6 + * Section 3, with β = 2, t = 53, s = 53 - 24 = 29, x = av. + * 53 is the number of effective mantissa bits in a Double; + * 24 in a Float. + * + * ◦ is the round-to-nearest operation with a tie-breaking + * rule (in our case, break-to-even). + * + * Let C = βˢ + 1 = 536870913 + * p = ◦(x × C) + * q = ◦(x − p) + * x₁ = ◦(p + q) + * + * Boldo proves that x₁ is the (t-s)-bit float closest to x, + * using the same tie-breaking rule as ◦. Since (t-s) = 24, + * this is the closest float32 (with 24 mantissa bits), and + * therefore the correct result of `fround`. + * + * Boldo also proves that if the computation of x × C does not + * cause overflow, then none of the following operations will + * cause overflow. We know that x (av) is less than the + * overflowThreshold, and overflowThreshold × C does not + * overflow, so that computation can never cause an overflow. + * + * If the reader does not have access to Boldo's paper, they + * may refer instead to + * Claude-Pierre Jeannerod, Jean-Michel Muller, Paul Zimmermann. + * On various ways to split a floating-point number. + * ARITH 2018 - 25th IEEE Symposium on Computer Arithmetic, + * Jun 2018, Amherst (MA), United States. + * pp.53-60, 10.1109/ARITH.2018.8464793. hal-01774587v2 + * available at + * https://hal.inria.fr/hal-01774587v2/document + * Section III, although that paper defers some theorems and + * proofs to Boldo's. + */ + const(p, av * 536870913), + Return(sign * (p + (av - p))) + ), { + /* Here, the result is represented as a subnormal form in a + * float32 representation. + * + * We round `av` to the nearest multiple of the smallest + * positive Float value (i.e., `Float.MinPositiveValue`), + * breaking ties to an even multiple. + * + * We do this by leveraging the inherent loss of precision near + * the minimum positive *double* value: conceptually, we divide + * the value by + * Float.MinPositiveValue / Double.MinPositiveValue + * which will drop the excess precision, applying exactly the + * rounding strategy that we want. Then we multiply the value + * back by the same constant. + * + * However, `Float.MinPositiveValue / Double.MinPositiveValue` + * is not representable as a finite Double. Therefore, we + * instead use the *inverse* constant + * Double.MinPositiveValue / Float.MinPositiveValue + * and we first multiply by that constant, then divide by it. + * + * --- + * + * As an additional "hack", the input values NaN, +0 and -0 + * also fall in this code path. For them, this computation + * happens to be an identity, and is therefore correct as well. + */ + val roundingFactor = double(Double.MinPositiveValue / Float.MinPositiveValue.toDouble) + Return(sign * ((av * roundingFactor) / roundingFactor)) + })) + )) + + If(typeof(Float32ArrayRef) !== str("undefined"), + typedArrayPolyfill, noTypedArrayPolyfill) case PrivateSymbolBuiltin => /* function privateJSFieldSymbol(description) { @@ -693,15 +689,11 @@ private[emitter] object CoreJSLib { }) }) }, { - if (strictFloats) { - If(genCallHelper(VarField.isFloat, instance), { - Return(constantClassResult(BoxedFloatClass)) - }, { - Return(constantClassResult(BoxedDoubleClass)) - }) - } else { + If(genCallHelper(VarField.isFloat, instance), { Return(constantClassResult(BoxedFloatClass)) - } + }, { + Return(constantClassResult(BoxedDoubleClass)) + }) }) ) }, @@ -1313,12 +1305,10 @@ private[emitter] object CoreJSLib { (Apply(genIdentBracketSelect(BigIntRef, "asIntN"), int(64) :: v :: Nil) === v)) } ) ::: - condDefs(strictFloats)( - defineFunction1(VarField.isFloat) { v => - Return((typeof(v) === str("number")) && - ((v !== v) || (genCallPolyfillableBuiltin(FroundBuiltin, v) === v))) - } - ) + defineFunction1(VarField.isFloat) { v => + Return((typeof(v) === str("number")) && + ((v !== v) || (genCallPolyfillableBuiltin(FroundBuiltin, v) === v))) + } } private def defineBoxFunctions(): List[Tree] = ( 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 38f73dfd8a..2ff301a7bf 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 @@ -437,7 +437,7 @@ private[emitter] final class SJSGen( case ShortType => genCallHelper(VarField.isShort, expr) case IntType => genCallHelper(VarField.isInt, expr) case LongType => genIsLong(expr) - case FloatType => genIsFloat(expr) + case FloatType => genCallHelper(VarField.isFloat, expr) case DoubleType => typeof(expr) === "number" case StringType => typeof(expr) === "string" @@ -478,7 +478,7 @@ private[emitter] final class SJSGen( case BoxedShortClass => genCallHelper(VarField.isShort, expr) case BoxedIntegerClass => genCallHelper(VarField.isInt, expr) case BoxedLongClass => genIsLong(expr) - case BoxedFloatClass => genIsFloat(expr) + case BoxedFloatClass => genCallHelper(VarField.isFloat, expr) case BoxedDoubleClass => typeof(expr) === "number" case BoxedStringClass => typeof(expr) === "string" } @@ -493,15 +493,6 @@ private[emitter] final class SJSGen( else expr instanceof globalVar(VarField.c, LongImpl.RuntimeLongClass) } - private def genIsFloat(expr: Tree)( - implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): Tree = { - import TreeDSL._ - - if (semantics.strictFloats) genCallHelper(VarField.isFloat, expr) - else typeof(expr) === "number" - } - def genAsInstanceOf(expr: Tree, tpe: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { @@ -524,8 +515,7 @@ private[emitter] final class SJSGen( case StringType => wg(expr || StringLiteral("")) case FloatType => - if (semantics.strictFloats) genCallPolyfillableBuiltin(FroundBuiltin, expr) - else wg(UnaryOp(irt.JSUnaryOp.+, expr)) + genCallPolyfillableBuiltin(FroundBuiltin, expr) case VoidType | NullType | NothingType | AnyNotNullType | ClassType(_, false) | ArrayType(_, false) | _:RecordType => diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 8ac39f199d..032ec1440e 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -17,6 +17,8 @@ object BinaryIncompatibilities { ) val LinkerInterface = Seq( + // private, not an issue + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.interface.Semantics.this"), ) val SbtPlugin = Seq( diff --git a/project/Build.scala b/project/Build.scala index 446aeebfab..d08f6224c9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2326,7 +2326,6 @@ object Build { "compliantNullPointers" -> (sems.nullPointers == CheckedBehavior.Compliant), "compliantStringIndexOutOfBounds" -> (sems.stringIndexOutOfBounds == CheckedBehavior.Compliant), "compliantModuleInit" -> (sems.moduleInit == CheckedBehavior.Compliant), - "strictFloats" -> sems.strictFloats, "productionMode" -> sems.productionMode, "esVersion" -> linkerConfig.esFeatures.esVersion.edition, "useECMAScript2015Semantics" -> linkerConfig.esFeatures.useECMAScript2015Semantics, diff --git a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala index d577790522..8356a0da7c 100644 --- a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala +++ b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala @@ -31,7 +31,6 @@ private[utils] object BuildInfo { final val compliantNullPointers = false final val compliantStringIndexOutOfBounds = false final val compliantModuleInit = false - final val strictFloats = false final val productionMode = false final val esVersion = 0 final val useECMAScript2015Semantics = false diff --git a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index 6251f85b88..65ab362539 100644 --- a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -88,40 +88,6 @@ object Platform { def hasDirectBuffers: Boolean = typedArrays - /** Do we use strict-floats semantics? - * - * If yes, `number` values that cannot be exactly represented as `Float` - * values respond `false` to an `isInstanceOf[Float]` test. If not, they - * respond `true`. - * - * In addition, if we use strict-float semantics, all arithmetic operations - * on `Float` are accurate wrt. 32-bit floating point semantics. In other - * words, `hasStrictFloats` implies `hasAccurateFloats`. - */ - def hasStrictFloats: Boolean = BuildInfo.strictFloats - - /** Are `Float` arithmetics accurate? - * - * If yes, the result of arithmetic operations on `Float`s, as well as - * `number.toFloat` operations, will be accurate wrt. IEEE-754 32-bit - * floating point operations. In other words, they behave exactly as - * specified on the JVM. - * - * This is true if either or both of the following are true: - * - * - We use strict-float semantics (see `hasStrictFloats`), or - * - The JavaScript runtime supports `Math.fround`. - * - * If neither is true, then the result of `Float` arithmetics can behave as - * if they were `Double` arithmetics instead. - * - * When the runtime does not support `Math.fround` but we strict-float - * semantics, Scala.js uses a semantically correct polyfill for it, which - * guarantees accurate float arithmetics. - */ - def hasAccurateFloats: Boolean = - hasStrictFloats || js.typeOf(js.Dynamic.global.Math.fround) != "undefined" - def regexSupportsUnicodeCase: Boolean = assumedESVersion >= ESVersion.ES2015 diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/FloatJSTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/FloatJSTest.scala index 612ea03d74..054f2bac59 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/FloatJSTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/FloatJSTest.scala @@ -17,8 +17,6 @@ import org.junit.Assert._ import org.scalajs.testsuite.utils.Requires -object FloatJSTest extends Requires.StrictFloats - class FloatJSTest { @noinline def froundNotInlined(x: Double): Float = diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/utils/Requires.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/utils/Requires.scala index c72fed474d..90e539e020 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/utils/Requires.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/utils/Requires.scala @@ -23,9 +23,4 @@ object Requires { assumeTrue("Assumed typed arrays are supported", typedArrays) } - trait StrictFloats { - @BeforeClass def needsTypedArrays(): Unit = - assumeTrue("Assumed strict floats", hasStrictFloats) - } - } diff --git a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index 6b480fd9b6..38e5007356 100644 --- a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -46,8 +46,6 @@ object Platform { def hasCompliantStringIndexOutOfBounds: Boolean = true def hasCompliantModule: Boolean = true def hasDirectBuffers: Boolean = true - def hasStrictFloats: Boolean = true - def hasAccurateFloats: Boolean = true def regexSupportsUnicodeCase: Boolean = true def regexSupportsUnicodeCharacterClasses: Boolean = true diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala index 6f158aa23e..f7b7d2005d 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala @@ -16,8 +16,6 @@ import org.junit.Test import org.junit.Assert._ import org.junit.Assume._ -import org.scalajs.testsuite.utils.Platform.hasAccurateFloats - class DoubleTest { final def assertExactEquals(expected: Double, actual: Double): Unit = assertTrue(s"expected: $expected; actual: $actual", expected.equals(actual)) @@ -62,8 +60,6 @@ class DoubleTest { def toFloat(): Unit = { // This is the closest we get to directly testing our `Math.fround` polyfill - assumeTrue("requires accurate floats", hasAccurateFloats) - @noinline def test(expected: Float, value: Double): Unit = assertExactEquals(s"for value $value", expected, value.toFloat) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala index d4dbdee940..60a4e40060 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala @@ -15,8 +15,6 @@ package org.scalajs.testsuite.compiler import org.junit.Test import org.junit.Assert._ -import org.scalajs.testsuite.utils.Platform.hasStrictFloats - class FloatTest { final def assertExactEquals(expected: Float, actual: Float): Unit = assertTrue(s"expected: $expected; actual: $actual", expected.equals(actual)) @@ -144,17 +142,15 @@ class FloatTest { // Non-special values // { val l = List(2.1f, 5.5f, -151.189f); for (n <- l; d <- l) println(s" test(${n % d}f, ${n}f, ${d}f)") } - if (hasStrictFloats) { - test(0.0f, 2.1f, 2.1f) - test(2.1f, 2.1f, 5.5f) - test(2.1f, 2.1f, -151.189f) - test(1.3000002f, 5.5f, 2.1f) - test(0.0f, 5.5f, 5.5f) - test(5.5f, 5.5f, -151.189f) - test(-2.0890021f, -151.189f, 2.1f) - test(-2.6889954f, -151.189f, 5.5f) - test(-0.0f, -151.189f, -151.189f) - } + test(0.0f, 2.1f, 2.1f) + test(2.1f, 2.1f, 5.5f) + test(2.1f, 2.1f, -151.189f) + test(1.3000002f, 5.5f, 2.1f) + test(0.0f, 5.5f, 5.5f) + test(5.5f, 5.5f, -151.189f) + test(-2.0890021f, -151.189f, 2.1f) + test(-2.6889954f, -151.189f, 5.5f) + test(-0.0f, -151.189f, -151.189f) } @Test @@ -244,10 +240,8 @@ class FloatTest { @inline def negate(x: Float): Float = -x - if (hasStrictFloats) { - assertExactEquals(0.8f, (hide(0.1f) + 0.3f) + 0.4f) - assertExactEquals(0.8000001f, 0.1f + (0.3f + hide(0.4f))) - } + assertExactEquals(0.8f, (hide(0.1f) + 0.3f) + 0.4f) + assertExactEquals(0.8000001f, 0.1f + (0.3f + hide(0.4f))) assertExactEquals(0.0f, 0.0f + hide(-0.0f)) assertExactEquals(0.0f, 0.0f - hide(0.0f)) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala index dd9ed7a5a6..108dc817de 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala @@ -609,8 +609,6 @@ class LongTest { } @Test def toFloat(): Unit = { - assumeTrue("Assumed accurate floats", hasAccurateFloats) - @inline def test(expected: Float, x: Long, epsilon: Float = 0.0f): Unit = { assertEquals(expected, x.toFloat, epsilon) assertEquals(expected, hideFromOptimizer(x).toFloat, epsilon) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RuntimeTypeTestsTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RuntimeTypeTestsTest.scala index 5916c9a301..1b73998241 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RuntimeTypeTestsTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RuntimeTypeTestsTest.scala @@ -198,7 +198,7 @@ class RuntimeTypeTestsTest { testFloat(false, 'e') testDouble(false, 'f') - testFloat(!hasStrictFloats, 1.2) + testFloat(false, 1.2) // Special cases for negative 0, NaN and Infinity diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala index a447e5c48b..4994137cf8 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala @@ -108,7 +108,7 @@ class ClassTest { test("java.lang.Float", -0.0f) test("java.lang.Float", 1.5f) test("java.lang.Float", Float.NaN) - test(if (hasStrictFloats) "java.lang.Double" else "java.lang.Float", 1.4) + test("java.lang.Double", 1.4) test("java.lang.String", "hello") test("java.lang.Object", new Object) test("scala.Some", Some(5)) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala index e16c2703d2..37bcc4838a 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala @@ -21,7 +21,7 @@ import java.lang.{Float => JFloat} import scala.util.Try import org.scalajs.testsuite.utils.AssertThrows.assertThrows -import org.scalajs.testsuite.utils.Platform.{executingInJVM, hasAccurateFloats} +import org.scalajs.testsuite.utils.Platform.executingInJVM class FloatTest { @@ -109,16 +109,9 @@ class FloatTest { @Test def parseStringMethods(): Unit = { def test(expected: Float, s: String): Unit = { - if (hasAccurateFloats) { - assertEquals(s, expected: Any, JFloat.parseFloat(s)) - assertEquals(s, expected: Any, JFloat.valueOf(s).floatValue()) - assertEquals(s, expected: Any, new JFloat(s).floatValue()) - } else { - val epsilon = Math.ulp(expected) - assertEquals(s, expected, JFloat.parseFloat(s), epsilon) - assertEquals(s, expected, JFloat.valueOf(s).floatValue(), epsilon) - assertEquals(s, expected, new JFloat(s).floatValue(), epsilon) - } + assertEquals(s, expected: Any, JFloat.parseFloat(s)) + assertEquals(s, expected: Any, JFloat.valueOf(s).floatValue()) + assertEquals(s, expected: Any, new JFloat(s).floatValue()) } // Specials diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ObjectTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ObjectTest.scala index 2868849763..cd86d36945 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ObjectTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ObjectTest.scala @@ -43,7 +43,7 @@ class ObjectTest { test(classOf[java.lang.Float], -0.0f) test(classOf[java.lang.Float], 1.5f) test(classOf[java.lang.Float], Float.NaN) - test(if (hasStrictFloats) classOf[java.lang.Double] else classOf[java.lang.Float], 1.4) + test(classOf[java.lang.Double], 1.4) test(classOf[java.lang.String], "hello") test(classOf[java.lang.Object], new Object) test(classOf[Some[_]], Some(5)) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala index 57533ab042..89c611735c 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala @@ -73,8 +73,6 @@ class BigDecimalConvertTest { } @Test def testFloatValueNegInfinity(): Unit = { - assumeTrue("requires accurate floats", hasAccurateFloats) - val a = "-123809648392384755735.63567887678287E+200" val aNumber = new BigDecimal(a) val result = Float.NegativeInfinity @@ -89,8 +87,6 @@ class BigDecimalConvertTest { } @Test def testFloatValuePosInfinity(): Unit = { - assumeTrue("requires accurate floats", hasAccurateFloats) - val a = "123809648373567356745735.6356789787678287E+200" val aNumber = new BigDecimal(a) val result = Float.PositiveInfinity @@ -99,14 +95,8 @@ class BigDecimalConvertTest { /** Test cases for `Float.parseFloat`, with an indirection through `BigDecimal`. */ @Test def testFloatValueLikeParseFloat_Issue4726(): Unit = { - def test(expected: Float, s: String): Unit = { - if (hasAccurateFloats) { - assertEquals(s, expected: Any, new BigDecimal(s).floatValue()) - } else { - val epsilon = Math.ulp(expected) - assertEquals(s, expected, new BigDecimal(s).floatValue(), epsilon) - } - } + def test(expected: Float, s: String): Unit = + assertEquals(s, expected: Any, new BigDecimal(s).floatValue()) // Zeros (BigDecimal has no negative 0, so they all parse to +0.0f) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala index c383d455f4..cc2e5cd3e6 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala @@ -255,8 +255,6 @@ class BigIntegerConvertTest { } @Test def testFloatValueNegativeInfinity2(): Unit = { - assumeTrue("requires accurate floats", hasAccurateFloats) - val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = -1 val aNumber = new BigInteger(aSign, a).floatValue() @@ -264,8 +262,6 @@ class BigIntegerConvertTest { } @Test def testFloatValueNegMantissaIsZero(): Unit = { - assumeTrue("requires accurate floats", hasAccurateFloats) - val a = Array[Byte](1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) val aSign = -1 val aNumber = new BigInteger(aSign, a).floatValue() @@ -300,8 +296,6 @@ class BigIntegerConvertTest { } @Test def testFloatValuePastNegMaxValue(): Unit = { - assumeTrue("requires accurate floats", hasAccurateFloats) - val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = -1 val aNumber = new BigInteger(aSign, a).floatValue() @@ -309,8 +303,6 @@ class BigIntegerConvertTest { } @Test def testFloatValuePastPosMaxValue(): Unit = { - assumeTrue("requires accurate floats", hasAccurateFloats) - val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = 1 val aNumber = new BigInteger(aSign, a).floatValue() @@ -333,8 +325,6 @@ class BigIntegerConvertTest { } @Test def testFloatValuePositiveInfinity1(): Unit = { - assumeTrue("requires accurate floats", hasAccurateFloats) - val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = 1 val aNumber: Float = new BigInteger(aSign, a).floatValue() @@ -648,8 +638,6 @@ class BigIntegerConvertTest { } @Test def testFloatValueBug2482(): Unit = { - assumeTrue("Assumed strict floats", hasStrictFloats) - val a = "2147483649" val result = 2.14748365E9f val aNumber = new BigInteger(a).floatValue() From dbce9db8527d81b9324f1daae1d347f945438a5e Mon Sep 17 00:00:00 2001 From: Leonid Dubinsky Date: Thu, 20 Feb 2025 15:01:06 -0500 Subject: [PATCH 10/30] JUnit: populate sbt.testing.Event.throwable on test failure If a Throwable associated with the test event is available, bubble it up through the SBT's test interface using the slot dedicated to just such information: `sbt.testing.Event.throwable`. In addition to it being a shame to throw away information about the test failure that is literally in our hands, this simplifies integration with runners that assume that test failure is always accompanied by a `Throwable` (e.g., Gradle). --- .../scala/org/scalajs/junit/Reporter.scala | 12 ++++--- .../junit/AssertEquals2TestAssertions_.txt | 2 +- .../junit/AssertEquals2TestAssertions_a.txt | 2 +- .../junit/AssertEquals2TestAssertions_n.txt | 2 +- .../junit/AssertEquals2TestAssertions_na.txt | 2 +- .../junit/AssertEquals2TestAssertions_nv.txt | 2 +- .../junit/AssertEquals2TestAssertions_nva.txt | 2 +- .../junit/AssertEquals2TestAssertions_nvc.txt | 2 +- .../AssertEquals2TestAssertions_nvca.txt | 2 +- .../junit/AssertEquals2TestAssertions_v.txt | 2 +- .../junit/AssertEquals2TestAssertions_va.txt | 2 +- .../junit/AssertEquals2TestAssertions_vc.txt | 2 +- .../junit/AssertEquals2TestAssertions_vs.txt | 2 +- .../junit/AssertEquals2TestAssertions_vsn.txt | 2 +- .../AssertEqualsDoubleTestAssertions_.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_a.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_n.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_na.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_nv.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_nva.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_nvc.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_nvca.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_v.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_va.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_vc.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_vs.txt | 12 +++---- .../AssertEqualsDoubleTestAssertions_vsn.txt | 12 +++---- .../junit/AssertEqualsTestAssertions_.txt | 2 +- .../junit/AssertEqualsTestAssertions_a.txt | 2 +- .../junit/AssertEqualsTestAssertions_n.txt | 2 +- .../junit/AssertEqualsTestAssertions_na.txt | 2 +- .../junit/AssertEqualsTestAssertions_nv.txt | 2 +- .../junit/AssertEqualsTestAssertions_nva.txt | 2 +- .../junit/AssertEqualsTestAssertions_nvc.txt | 2 +- .../junit/AssertEqualsTestAssertions_nvca.txt | 2 +- .../junit/AssertEqualsTestAssertions_v.txt | 2 +- .../junit/AssertEqualsTestAssertions_va.txt | 2 +- .../junit/AssertEqualsTestAssertions_vc.txt | 2 +- .../junit/AssertEqualsTestAssertions_vs.txt | 2 +- .../junit/AssertEqualsTestAssertions_vsn.txt | 2 +- .../junit/AssertFalse2TestAssertions_.txt | 2 +- .../junit/AssertFalse2TestAssertions_a.txt | 2 +- .../junit/AssertFalse2TestAssertions_n.txt | 2 +- .../junit/AssertFalse2TestAssertions_na.txt | 2 +- .../junit/AssertFalse2TestAssertions_nv.txt | 2 +- .../junit/AssertFalse2TestAssertions_nva.txt | 2 +- .../junit/AssertFalse2TestAssertions_nvc.txt | 2 +- .../junit/AssertFalse2TestAssertions_nvca.txt | 2 +- .../junit/AssertFalse2TestAssertions_v.txt | 2 +- .../junit/AssertFalse2TestAssertions_va.txt | 2 +- .../junit/AssertFalse2TestAssertions_vc.txt | 2 +- .../junit/AssertFalse2TestAssertions_vs.txt | 2 +- .../junit/AssertFalse2TestAssertions_vsn.txt | 2 +- .../junit/AssertFalseTestAssertions_.txt | 2 +- .../junit/AssertFalseTestAssertions_a.txt | 2 +- .../junit/AssertFalseTestAssertions_n.txt | 2 +- .../junit/AssertFalseTestAssertions_na.txt | 2 +- .../junit/AssertFalseTestAssertions_nv.txt | 2 +- .../junit/AssertFalseTestAssertions_nva.txt | 2 +- .../junit/AssertFalseTestAssertions_nvc.txt | 2 +- .../junit/AssertFalseTestAssertions_nvca.txt | 2 +- .../junit/AssertFalseTestAssertions_v.txt | 2 +- .../junit/AssertFalseTestAssertions_va.txt | 2 +- .../junit/AssertFalseTestAssertions_vc.txt | 2 +- .../junit/AssertFalseTestAssertions_vs.txt | 2 +- .../junit/AssertFalseTestAssertions_vsn.txt | 2 +- .../AssertStringEqualsTestAssertions_.txt | 2 +- .../AssertStringEqualsTestAssertions_a.txt | 2 +- .../AssertStringEqualsTestAssertions_n.txt | 2 +- .../AssertStringEqualsTestAssertions_na.txt | 2 +- .../AssertStringEqualsTestAssertions_nv.txt | 2 +- .../AssertStringEqualsTestAssertions_nva.txt | 2 +- .../AssertStringEqualsTestAssertions_nvc.txt | 2 +- .../AssertStringEqualsTestAssertions_nvca.txt | 2 +- .../AssertStringEqualsTestAssertions_v.txt | 2 +- .../AssertStringEqualsTestAssertions_va.txt | 2 +- .../AssertStringEqualsTestAssertions_vc.txt | 2 +- .../AssertStringEqualsTestAssertions_vs.txt | 2 +- .../AssertStringEqualsTestAssertions_vsn.txt | 2 +- .../junit/AssertTrueTestAssertions_.txt | 4 +-- .../junit/AssertTrueTestAssertions_a.txt | 4 +-- .../junit/AssertTrueTestAssertions_n.txt | 4 +-- .../junit/AssertTrueTestAssertions_na.txt | 4 +-- .../junit/AssertTrueTestAssertions_nv.txt | 4 +-- .../junit/AssertTrueTestAssertions_nva.txt | 4 +-- .../junit/AssertTrueTestAssertions_nvc.txt | 4 +-- .../junit/AssertTrueTestAssertions_nvca.txt | 4 +-- .../junit/AssertTrueTestAssertions_v.txt | 4 +-- .../junit/AssertTrueTestAssertions_va.txt | 4 +-- .../junit/AssertTrueTestAssertions_vc.txt | 4 +-- .../junit/AssertTrueTestAssertions_vs.txt | 4 +-- .../junit/AssertTrueTestAssertions_vsn.txt | 4 +-- .../junit/AssumeAfterAssumeAssertions_.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_a.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_n.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_na.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_nv.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_nva.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_nvc.txt | 2 +- .../AssumeAfterAssumeAssertions_nvca.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_v.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_va.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_vc.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_vs.txt | 2 +- .../junit/AssumeAfterAssumeAssertions_vsn.txt | 2 +- .../junit/AssumeAfterClassTestAssertions_.txt | 4 +-- .../AssumeAfterClassTestAssertions_a.txt | 4 +-- .../AssumeAfterClassTestAssertions_n.txt | 4 +-- .../AssumeAfterClassTestAssertions_na.txt | 4 +-- .../AssumeAfterClassTestAssertions_nv.txt | 4 +-- .../AssumeAfterClassTestAssertions_nva.txt | 4 +-- .../AssumeAfterClassTestAssertions_nvc.txt | 4 +-- .../AssumeAfterClassTestAssertions_nvca.txt | 4 +-- .../AssumeAfterClassTestAssertions_v.txt | 4 +-- .../AssumeAfterClassTestAssertions_va.txt | 4 +-- .../AssumeAfterClassTestAssertions_vc.txt | 4 +-- .../AssumeAfterClassTestAssertions_vs.txt | 4 +-- .../AssumeAfterClassTestAssertions_vsn.txt | 4 +-- .../junit/AssumeAfterExceptionAssertions_.txt | 2 +- .../AssumeAfterExceptionAssertions_a.txt | 2 +- .../AssumeAfterExceptionAssertions_n.txt | 2 +- .../AssumeAfterExceptionAssertions_na.txt | 2 +- .../AssumeAfterExceptionAssertions_nv.txt | 2 +- .../AssumeAfterExceptionAssertions_nva.txt | 2 +- .../AssumeAfterExceptionAssertions_nvc.txt | 2 +- .../AssumeAfterExceptionAssertions_nvca.txt | 2 +- .../AssumeAfterExceptionAssertions_v.txt | 2 +- .../AssumeAfterExceptionAssertions_va.txt | 2 +- .../AssumeAfterExceptionAssertions_vc.txt | 2 +- .../AssumeAfterExceptionAssertions_vs.txt | 2 +- .../AssumeAfterExceptionAssertions_vsn.txt | 2 +- .../junit/AssumeInAfterAssertions_.txt | 2 +- .../junit/AssumeInAfterAssertions_a.txt | 2 +- .../junit/AssumeInAfterAssertions_n.txt | 2 +- .../junit/AssumeInAfterAssertions_na.txt | 2 +- .../junit/AssumeInAfterAssertions_nv.txt | 2 +- .../junit/AssumeInAfterAssertions_nva.txt | 2 +- .../junit/AssumeInAfterAssertions_nvc.txt | 2 +- .../junit/AssumeInAfterAssertions_nvca.txt | 2 +- .../junit/AssumeInAfterAssertions_v.txt | 2 +- .../junit/AssumeInAfterAssertions_va.txt | 2 +- .../junit/AssumeInAfterAssertions_vc.txt | 2 +- .../junit/AssumeInAfterAssertions_vs.txt | 2 +- .../junit/AssumeInAfterAssertions_vsn.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_a.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_n.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_na.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_nv.txt | 2 +- .../junit/AssumeTestAssertions_nva.txt | 2 +- .../junit/AssumeTestAssertions_nvc.txt | 2 +- .../junit/AssumeTestAssertions_nvca.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_v.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_va.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_vc.txt | 2 +- .../scalajs/junit/AssumeTestAssertions_vs.txt | 2 +- .../junit/AssumeTestAssertions_vsn.txt | 2 +- .../scalajs/junit/AsyncTestAssertions_.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_a.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_n.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_na.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_nv.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_nva.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_nvc.txt | 6 ++-- .../junit/AsyncTestAssertions_nvca.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_v.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_va.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_vc.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_vs.txt | 6 ++-- .../scalajs/junit/AsyncTestAssertions_vsn.txt | 6 ++-- .../junit/BeforeAndAfterTestAssertions_.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_a.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_n.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_na.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_nv.txt | 2 +- .../BeforeAndAfterTestAssertions_nva.txt | 2 +- .../BeforeAndAfterTestAssertions_nvc.txt | 2 +- .../BeforeAndAfterTestAssertions_nvca.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_v.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_va.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_vc.txt | 2 +- .../junit/BeforeAndAfterTestAssertions_vs.txt | 2 +- .../BeforeAndAfterTestAssertions_vsn.txt | 2 +- .../junit/BeforeAssumeFailTestAssertions_.txt | 2 +- .../BeforeAssumeFailTestAssertions_a.txt | 2 +- .../BeforeAssumeFailTestAssertions_n.txt | 2 +- .../BeforeAssumeFailTestAssertions_na.txt | 2 +- .../BeforeAssumeFailTestAssertions_nv.txt | 2 +- .../BeforeAssumeFailTestAssertions_nva.txt | 2 +- .../BeforeAssumeFailTestAssertions_nvc.txt | 2 +- .../BeforeAssumeFailTestAssertions_nvca.txt | 2 +- .../BeforeAssumeFailTestAssertions_v.txt | 2 +- .../BeforeAssumeFailTestAssertions_va.txt | 2 +- .../BeforeAssumeFailTestAssertions_vc.txt | 2 +- .../BeforeAssumeFailTestAssertions_vs.txt | 2 +- .../BeforeAssumeFailTestAssertions_vsn.txt | 2 +- .../junit/ExceptionAfterAssumeAssertions_.txt | 2 +- .../ExceptionAfterAssumeAssertions_a.txt | 2 +- .../ExceptionAfterAssumeAssertions_n.txt | 2 +- .../ExceptionAfterAssumeAssertions_na.txt | 2 +- .../ExceptionAfterAssumeAssertions_nv.txt | 2 +- .../ExceptionAfterAssumeAssertions_nva.txt | 2 +- .../ExceptionAfterAssumeAssertions_nvc.txt | 2 +- .../ExceptionAfterAssumeAssertions_nvca.txt | 2 +- .../ExceptionAfterAssumeAssertions_v.txt | 2 +- .../ExceptionAfterAssumeAssertions_va.txt | 2 +- .../ExceptionAfterAssumeAssertions_vc.txt | 2 +- .../ExceptionAfterAssumeAssertions_vs.txt | 2 +- .../ExceptionAfterAssumeAssertions_vsn.txt | 2 +- .../junit/ExceptionAfterClassAssertions_.txt | 6 ++-- .../junit/ExceptionAfterClassAssertions_a.txt | 6 ++-- .../junit/ExceptionAfterClassAssertions_n.txt | 6 ++-- .../ExceptionAfterClassAssertions_na.txt | 6 ++-- .../ExceptionAfterClassAssertions_nv.txt | 6 ++-- .../ExceptionAfterClassAssertions_nva.txt | 6 ++-- .../ExceptionAfterClassAssertions_nvc.txt | 6 ++-- .../ExceptionAfterClassAssertions_nvca.txt | 6 ++-- .../junit/ExceptionAfterClassAssertions_v.txt | 6 ++-- .../ExceptionAfterClassAssertions_va.txt | 6 ++-- .../ExceptionAfterClassAssertions_vc.txt | 6 ++-- .../ExceptionAfterClassAssertions_vs.txt | 6 ++-- .../ExceptionAfterClassAssertions_vsn.txt | 6 ++-- ...xceptionBeforeAndAfterClassAssertions_.txt | 2 +- ...ceptionBeforeAndAfterClassAssertions_a.txt | 2 +- ...ceptionBeforeAndAfterClassAssertions_n.txt | 2 +- ...eptionBeforeAndAfterClassAssertions_na.txt | 2 +- ...eptionBeforeAndAfterClassAssertions_nv.txt | 2 +- ...ptionBeforeAndAfterClassAssertions_nva.txt | 2 +- ...ptionBeforeAndAfterClassAssertions_nvc.txt | 2 +- ...tionBeforeAndAfterClassAssertions_nvca.txt | 2 +- ...ceptionBeforeAndAfterClassAssertions_v.txt | 2 +- ...eptionBeforeAndAfterClassAssertions_va.txt | 2 +- ...eptionBeforeAndAfterClassAssertions_vc.txt | 2 +- ...eptionBeforeAndAfterClassAssertions_vs.txt | 2 +- ...ptionBeforeAndAfterClassAssertions_vsn.txt | 2 +- .../junit/ExceptionBeforeClassAssertions_.txt | 2 +- .../ExceptionBeforeClassAssertions_a.txt | 2 +- .../ExceptionBeforeClassAssertions_n.txt | 2 +- .../ExceptionBeforeClassAssertions_na.txt | 2 +- .../ExceptionBeforeClassAssertions_nv.txt | 2 +- .../ExceptionBeforeClassAssertions_nva.txt | 2 +- .../ExceptionBeforeClassAssertions_nvc.txt | 2 +- .../ExceptionBeforeClassAssertions_nvca.txt | 2 +- .../ExceptionBeforeClassAssertions_v.txt | 2 +- .../ExceptionBeforeClassAssertions_va.txt | 2 +- .../ExceptionBeforeClassAssertions_vc.txt | 2 +- .../ExceptionBeforeClassAssertions_vs.txt | 2 +- .../ExceptionBeforeClassAssertions_vsn.txt | 2 +- .../junit/ExceptionEverywhereAssertions_.txt | 2 +- .../junit/ExceptionEverywhereAssertions_a.txt | 2 +- .../junit/ExceptionEverywhereAssertions_n.txt | 2 +- .../ExceptionEverywhereAssertions_na.txt | 2 +- .../ExceptionEverywhereAssertions_nv.txt | 2 +- .../ExceptionEverywhereAssertions_nva.txt | 2 +- .../ExceptionEverywhereAssertions_nvc.txt | 2 +- .../ExceptionEverywhereAssertions_nvca.txt | 2 +- .../junit/ExceptionEverywhereAssertions_v.txt | 2 +- .../ExceptionEverywhereAssertions_va.txt | 2 +- .../ExceptionEverywhereAssertions_vc.txt | 2 +- .../ExceptionEverywhereAssertions_vs.txt | 2 +- .../ExceptionEverywhereAssertions_vsn.txt | 2 +- .../junit/ExceptionInAfterTestAssertions_.txt | 2 +- .../ExceptionInAfterTestAssertions_a.txt | 2 +- .../ExceptionInAfterTestAssertions_n.txt | 2 +- .../ExceptionInAfterTestAssertions_na.txt | 2 +- .../ExceptionInAfterTestAssertions_nv.txt | 2 +- .../ExceptionInAfterTestAssertions_nva.txt | 2 +- .../ExceptionInAfterTestAssertions_nvc.txt | 2 +- .../ExceptionInAfterTestAssertions_nvca.txt | 2 +- .../ExceptionInAfterTestAssertions_v.txt | 2 +- .../ExceptionInAfterTestAssertions_va.txt | 2 +- .../ExceptionInAfterTestAssertions_vc.txt | 2 +- .../ExceptionInAfterTestAssertions_vs.txt | 2 +- .../ExceptionInAfterTestAssertions_vsn.txt | 2 +- .../ExceptionInBeforeTestAssertions_.txt | 2 +- .../ExceptionInBeforeTestAssertions_a.txt | 2 +- .../ExceptionInBeforeTestAssertions_n.txt | 2 +- .../ExceptionInBeforeTestAssertions_na.txt | 2 +- .../ExceptionInBeforeTestAssertions_nv.txt | 2 +- .../ExceptionInBeforeTestAssertions_nva.txt | 2 +- .../ExceptionInBeforeTestAssertions_nvc.txt | 2 +- .../ExceptionInBeforeTestAssertions_nvca.txt | 2 +- .../ExceptionInBeforeTestAssertions_v.txt | 2 +- .../ExceptionInBeforeTestAssertions_va.txt | 2 +- .../ExceptionInBeforeTestAssertions_vc.txt | 2 +- .../ExceptionInBeforeTestAssertions_vs.txt | 2 +- .../ExceptionInBeforeTestAssertions_vsn.txt | 2 +- .../ExceptionInConstructorTestAssertions_.txt | 2 +- ...ExceptionInConstructorTestAssertions_a.txt | 2 +- ...ExceptionInConstructorTestAssertions_n.txt | 2 +- ...xceptionInConstructorTestAssertions_na.txt | 2 +- ...xceptionInConstructorTestAssertions_nv.txt | 2 +- ...ceptionInConstructorTestAssertions_nva.txt | 2 +- ...ceptionInConstructorTestAssertions_nvc.txt | 2 +- ...eptionInConstructorTestAssertions_nvca.txt | 2 +- ...ExceptionInConstructorTestAssertions_v.txt | 2 +- ...xceptionInConstructorTestAssertions_va.txt | 2 +- ...xceptionInConstructorTestAssertions_vc.txt | 2 +- ...xceptionInConstructorTestAssertions_vs.txt | 2 +- ...ceptionInConstructorTestAssertions_vsn.txt | 2 +- .../junit/ExceptionTestAssertions_.txt | 2 +- .../junit/ExceptionTestAssertions_a.txt | 2 +- .../junit/ExceptionTestAssertions_n.txt | 2 +- .../junit/ExceptionTestAssertions_na.txt | 2 +- .../junit/ExceptionTestAssertions_nv.txt | 2 +- .../junit/ExceptionTestAssertions_nva.txt | 2 +- .../junit/ExceptionTestAssertions_nvc.txt | 2 +- .../junit/ExceptionTestAssertions_nvca.txt | 2 +- .../junit/ExceptionTestAssertions_v.txt | 2 +- .../junit/ExceptionTestAssertions_va.txt | 2 +- .../junit/ExceptionTestAssertions_vc.txt | 2 +- .../junit/ExceptionTestAssertions_vs.txt | 2 +- .../junit/ExceptionTestAssertions_vsn.txt | 2 +- .../scalajs/junit/ExpectTestAssertions_.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_a.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_n.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_na.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_nv.txt | 10 +++--- .../junit/ExpectTestAssertions_nva.txt | 10 +++--- .../junit/ExpectTestAssertions_nvc.txt | 10 +++--- .../junit/ExpectTestAssertions_nvca.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_v.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_va.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_vc.txt | 10 +++--- .../scalajs/junit/ExpectTestAssertions_vs.txt | 10 +++--- .../junit/ExpectTestAssertions_vsn.txt | 10 +++--- .../scalajs/junit/IgnoreTestAssertions_.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_a.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_n.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_na.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_nv.txt | 2 +- .../junit/IgnoreTestAssertions_nva.txt | 2 +- .../junit/IgnoreTestAssertions_nvc.txt | 2 +- .../junit/IgnoreTestAssertions_nvca.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_v.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_va.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_vc.txt | 2 +- .../scalajs/junit/IgnoreTestAssertions_vs.txt | 2 +- .../junit/IgnoreTestAssertions_vsn.txt | 2 +- .../junit/MethodNameDecodeTestAssertions_.txt | 2 +- .../MethodNameDecodeTestAssertions_a.txt | 2 +- .../MethodNameDecodeTestAssertions_n.txt | 2 +- .../MethodNameDecodeTestAssertions_na.txt | 2 +- .../MethodNameDecodeTestAssertions_nv.txt | 2 +- .../MethodNameDecodeTestAssertions_nva.txt | 2 +- .../MethodNameDecodeTestAssertions_nvc.txt | 2 +- .../MethodNameDecodeTestAssertions_nvca.txt | 2 +- .../MethodNameDecodeTestAssertions_v.txt | 2 +- .../MethodNameDecodeTestAssertions_va.txt | 2 +- .../MethodNameDecodeTestAssertions_vc.txt | 2 +- .../MethodNameDecodeTestAssertions_vs.txt | 2 +- .../MethodNameDecodeTestAssertions_vsn.txt | 2 +- .../scalajs/junit/Multi1TestAssertions_.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_a.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_n.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_na.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_nv.txt | 4 +-- .../junit/Multi1TestAssertions_nva.txt | 4 +-- .../junit/Multi1TestAssertions_nvc.txt | 4 +-- .../junit/Multi1TestAssertions_nvca.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_v.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_va.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_vc.txt | 4 +-- .../scalajs/junit/Multi1TestAssertions_vs.txt | 4 +-- .../junit/Multi1TestAssertions_vsn.txt | 4 +-- .../scalajs/junit/Multi2TestAssertions_.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_a.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_n.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_na.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_nv.txt | 10 +++--- .../junit/Multi2TestAssertions_nva.txt | 10 +++--- .../junit/Multi2TestAssertions_nvc.txt | 10 +++--- .../junit/Multi2TestAssertions_nvca.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_v.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_va.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_vc.txt | 10 +++--- .../scalajs/junit/Multi2TestAssertions_vs.txt | 10 +++--- .../junit/Multi2TestAssertions_vsn.txt | 10 +++--- .../junit/MultiAssumeFail1TestAssertions_.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_a.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_n.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_na.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_nv.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_nva.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_nvc.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_nvca.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_v.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_va.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_vc.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_vs.txt | 10 +++--- .../MultiAssumeFail1TestAssertions_vsn.txt | 10 +++--- .../junit/MultiAssumeFail2TestAssertions_.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_a.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_n.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_na.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_nv.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_nva.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_nvc.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_nvca.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_v.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_va.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_vc.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_vs.txt | 10 +++--- .../MultiAssumeFail2TestAssertions_vsn.txt | 10 +++--- .../MultiBeforeAssumeFailTestAssertions_.txt | 2 +- .../MultiBeforeAssumeFailTestAssertions_a.txt | 2 +- .../MultiBeforeAssumeFailTestAssertions_n.txt | 2 +- ...MultiBeforeAssumeFailTestAssertions_na.txt | 2 +- ...MultiBeforeAssumeFailTestAssertions_nv.txt | 2 +- ...ultiBeforeAssumeFailTestAssertions_nva.txt | 2 +- ...ultiBeforeAssumeFailTestAssertions_nvc.txt | 2 +- ...ltiBeforeAssumeFailTestAssertions_nvca.txt | 2 +- .../MultiBeforeAssumeFailTestAssertions_v.txt | 2 +- ...MultiBeforeAssumeFailTestAssertions_va.txt | 2 +- ...MultiBeforeAssumeFailTestAssertions_vc.txt | 2 +- ...MultiBeforeAssumeFailTestAssertions_vs.txt | 2 +- ...ultiBeforeAssumeFailTestAssertions_vsn.txt | 2 +- .../junit/MultiIgnore1TestAssertions_.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_a.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_n.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_na.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_nv.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_nva.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_nvc.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_nvca.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_v.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_va.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_vc.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_vs.txt | 10 +++--- .../junit/MultiIgnore1TestAssertions_vsn.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_a.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_n.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_na.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_nv.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_nva.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_nvc.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_nvca.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_v.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_va.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_vc.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_vs.txt | 10 +++--- .../junit/MultiIgnore2TestAssertions_vsn.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_a.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_n.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_na.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_nv.txt | 10 +++--- .../MultiIgnoreAllTestAssertions_nva.txt | 10 +++--- .../MultiIgnoreAllTestAssertions_nvc.txt | 10 +++--- .../MultiIgnoreAllTestAssertions_nvca.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_v.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_va.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_vc.txt | 10 +++--- .../junit/MultiIgnoreAllTestAssertions_vs.txt | 10 +++--- .../MultiIgnoreAllTestAssertions_vsn.txt | 10 +++--- .../org/scalajs/junit/utils/JUnitTest.scala | 31 ++++++++++++++----- 457 files changed, 1006 insertions(+), 987 deletions(-) diff --git a/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala b/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala index 4673a4cf9e..4cdbfbeb9f 100644 --- a/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala +++ b/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala @@ -59,7 +59,7 @@ private[junit] final class Reporter(eventHandler: EventHandler, if (errors.nonEmpty) { emit(errors.head) - emitEvent(method, Status.Failure) + emitEvent(method, Status.Failure, new OptionalThrowable(errors.head)) errors.tail.foreach(emit) } } @@ -67,7 +67,7 @@ private[junit] final class Reporter(eventHandler: EventHandler, def reportAssumptionViolation(method: Option[String], timeInSeconds: Double, e: Throwable): Unit = { logTestException(_.warn, "Test assumption in test ", method, e, timeInSeconds) - emitEvent(method, Status.Skipped) + emitEvent(method, Status.Skipped, new OptionalThrowable(e)) } private def logTestInfo(level: Reporter.Level, method: Option[String], msg: String): Unit = @@ -114,11 +114,15 @@ private[junit] final class Reporter(eventHandler: EventHandler, prefix + Ansi.c(name, color) } - private def emitEvent(method: Option[String], status: Status): Unit = { + private def emitEvent( + method: Option[String], + status: Status, + throwable: OptionalThrowable = new OptionalThrowable + ): Unit = { val testName = method.fold(taskDef.fullyQualifiedName())(method => taskDef.fullyQualifiedName() + "." + settings.decodeName(method)) val selector = new TestSelector(testName) - eventHandler.handle(new JUnitEvent(taskDef, status, selector)) + eventHandler.handle(new JUnitEvent(taskDef, status, selector, throwable)) } def log(level: Reporter.Level, s: String): Unit = { diff --git a/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt b/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt index 2a161db9ae..2072559893 100644 --- a/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt +++ b/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt @@ -1,7 +1,7 @@ ldTest run started ldTest org.scalajs.junit.AssertEquals2Test.test started leTest org.scalajs.junit.AssertEquals2Test.test failed: This is the message expected: but was:, took