From 864ebaef272c6830e3d633b9e17b121612ba46dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 1 Jun 2025 19:04:08 +0200 Subject: [PATCH] Optimize divisions and remainders by constants. Following the techniques described in Hacker's Delight, Chapter 10. --- .../src/main/scala/org/scalajs/ir/Trees.scala | 7 + .../frontend/optimizer/IntegerDivisions.scala | 547 ++++++++++++++++ .../frontend/optimizer/OptimizerCore.scala | 34 + .../optimizer/ElementaryInterpreter.scala | 157 +++++ .../optimizer/IntegerDivisionsTest.scala | 591 ++++++++++++++++++ .../scalajs/linker/testutils/TreeDSL.scala | 40 ++ project/Build.scala | 22 +- 7 files changed, 1387 insertions(+), 11 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisions.scala create mode 100644 linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/ElementaryInterpreter.scala create mode 100644 linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisionsTest.scala create mode 100644 linker/shared/src/test/scala/org/scalajs/linker/testutils/TreeDSL.scala diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index ca2c76dcc8..53ceab688c 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -125,6 +125,13 @@ object Trees { override def toString(): String = stats.mkString("Block(", ",", ")") + + override def equals(that: Any): Boolean = that match { + case that: Block => this.stats == that.stats + case _ => false + } + + override def hashCode(): Int = stats.## } object Block { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisions.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisions.scala new file mode 100644 index 0000000000..bbb58aa8cb --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisions.scala @@ -0,0 +1,547 @@ +/* + * 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.optimizer + +import java.lang.{Long => JLong} + +import org.scalajs.ir.Position +import org.scalajs.ir.Names._ +import org.scalajs.ir.OriginalName.NoOriginalName +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ +import org.scalajs.ir.WellKnownNames._ + +import org.scalajs.linker.backend.emitter.LongImpl + +private[optimizer] final class IntegerDivisions(useRuntimeLong: Boolean) { + import IntegerDivisions._ + + def makeOptimizedDivision[T](op: BinaryOp.Code, divisor: T)( + implicit int: UnsignedIntegral[T], pos: Position): Tree = { + import int.mkNumericOps + + val argRef = VarRef(NumeratorArgName)(int.tpe) + + val (isSigned, isQuotient) = op match { + case BinaryOp.Int_/ | BinaryOp.Long_/ => (true, true) + case BinaryOp.Int_% | BinaryOp.Long_% => (true, false) + case BinaryOp.Int_unsigned_/ | BinaryOp.Long_unsigned_/ => (false, true) + case BinaryOp.Int_unsigned_% | BinaryOp.Long_unsigned_% => (false, false) + } + + val negativeDivisor = isSigned && int.lt(divisor, int.zero) + val absDivisor = if (negativeDivisor) -divisor else divisor + + if (absDivisor == int.zero) { + UnaryOp( + UnaryOp.Throw, + New( + ArithmeticExceptionClass, + MethodIdent(MethodName.constructor(List(ClassRef(BoxedStringClass)))), + List(StringLiteral("/ by zero")) + ) + ) + } else if (absDivisor == int.one) { + // The algorithms below assume absDivisor >= 2, so special-case 1 + if (isQuotient) { + if (negativeDivisor) + BinaryOp(int.Op_-, int.literalZero, argRef) + else + argRef + } else { + int.literalZero + } + } else if (int.isUnsignedPowerOf2(absDivisor)) { + val k = int.unsignedFloorLog2(absDivisor) // i.e., absDivisor == 2^k + + if (isSigned) { + // Sections 10-1 (quotient) and 10-2 (remainder) + + val t1 = BinaryOp(int.Op_>>, argRef, IntLiteral(k - 1)) + val t2 = BinaryOp(int.Op_>>>, t1, IntLiteral(int.bitSize - k)) + + if (isQuotient) { + val t3 = BinaryOp(int.Op_+, argRef, t2) + val q = BinaryOp(int.Op_>>, t3, IntLiteral(k)) + + if (negativeDivisor) + BinaryOp(int.Op_-, int.literalZero, q) + else + q + } else { + val temp1Def = tempVarDef(tName, t2) + Block( + temp1Def, + // ((arg + temp1) & (2^k - 1)) - temp1 + BinaryOp( + int.Op_-, + BinaryOp( + int.Op_&, + BinaryOp(int.Op_+, argRef, temp1Def.ref), + int.literal(absDivisor - int.one) + ), + temp1Def.ref + ) + ) + } + } else { + // Unsigned is straightforward + if (isQuotient) + BinaryOp(int.Op_>>>, argRef, IntLiteral(k)) + else + BinaryOp(int.Op_&, argRef, int.literal(absDivisor - int.one)) + } + } else { + val quotient: Tree = if (isSigned) { + // TODO use normal negativeDivisor and optimize the code according to Section 10-5 + val data = computeSignedMagic(absDivisor, negativeDivisor = false) + + var q = int.genMulSignedHi(data.M, argRef, useRuntimeLong) + if (data.add != 0) + q = BinaryOp(int.Op_+, q, argRef) + q = BinaryOp(int.Op_>>, q, IntLiteral(data.shift)) + q = BinaryOp(int.Op_+, q, BinaryOp(int.Op_>>>, argRef, IntLiteral(int.bitSize - 1))) + + if (negativeDivisor) + BinaryOp(int.Op_-, int.literalZero, q) + else + q + } else { + // Hacker's Delight, Section 10-8 + val data = computeUnsignedMagic(absDivisor, negativeDivisor) + + val computeMulHi = int.genMulUnsignedHi(data.M, argRef) + if (data.add != 0) { + // Granlund-Montgomery trick to avoid the 33/65-bit quantity + val hiDef = tempVarDef(hiName, computeMulHi) // stored in `q` in the original algorithm + val hi = hiDef.ref + var t = BinaryOp(int.Op_-, argRef, hi) + t = BinaryOp(int.Op_>>>, t, IntLiteral(1)) + t = BinaryOp(int.Op_+, t, hi) + t = BinaryOp(int.Op_>>>, t, IntLiteral(data.shift - 1)) + Block(hiDef, t) + } else { + BinaryOp(int.Op_>>>, computeMulHi, IntLiteral(data.shift)) + } + } + + if (isQuotient) { + quotient + } else { + // r = n - d*q + BinaryOp(int.Op_-, argRef, BinaryOp(int.Op_*, int.literal(divisor), quotient)) + } + } + } +} + +private[optimizer] object IntegerDivisions { + /** Local argument name for the numerator, used by the generated code. + * + * The optimizer should bind this name to the numerator in the scope of the + * code generated by `makeOptimizedDivision` (as if it had been inlined from + * a method taking `NumeratorArgName` as formal parameter). + */ + val NumeratorArgName = LocalName("num") + + private[optimizer] val tName = LocalName("t") // accessible to tests + private[optimizer] val hiName = LocalName("hi") // accessible to tests + private val y0Name = LocalName("y0") + private val y1Name = LocalName("y1") + private val mulhiTempName = LocalName("mulht") + + private def tempVarDef(name: LocalName, rhs: Tree)(implicit pos: Position): VarDef = + VarDef(LocalIdent(name), NoOriginalName, rhs.tpe, mutable = false, rhs) + + /** Magic data, from which we derive the code to generate. + * + * `add` is +1 if we need an addition, -1 for a subtraction, and 0 for none. + */ + private[optimizer] final case class MagicData[T](M: T, add: Int, shift: Int) + + // private[optimizer] for unit tests + private[optimizer] def computeSignedMagic[T](ad: T, negativeDivisor: Boolean)( + implicit int: UnsignedIntegral[T]): MagicData[T] = { + import int.mkNumericOps + + val two31 = int.one_<<(int.bitSize - 1) + + val t = two31 + (if (negativeDivisor) int.one else int.zero) + val anc = t - int.one - (int.remainderUnsigned(t, ad)) // absolute value of nc + + var p = int.bitSize - 1 // init p + var q1 = int.divideUnsigned(two31, anc) // init q1 = 2**p / |nc| + var r1 = two31 - q1 * anc // init r1 = rem(2**p, |nc|) + var q2 = int.divideUnsigned(two31, ad) // init q2 = 2**p / |d| + var r2 = two31 - q2 * ad // init r2 = rem(2**p, |d|) + + while ({ + // do + p += 1 + + q1 = int.times2(q1) + r1 = int.times2(r1) + if (int.compareUnsigned(r1, anc) >= 0) { + q1 += int.one + r1 -= anc + } + + q2 = int.times2(q2) + r2 = int.times2(r2) + if (int.compareUnsigned(r2, ad) >= 0) { + q2 += int.one + r2 -= ad + } + + val delta = ad - r2 + // while + (int.compareUnsigned(q1, delta) < 0 || (q1 == delta && r1 == int.zero)) + }) {} + + val M = + if (negativeDivisor) -(q2 + int.one) + else q2 + int.one + val a = + if (!negativeDivisor && int.lt(M, int.zero)) +1 + else if (negativeDivisor && int.gt(M, int.zero)) -1 + else 0 + val s = p - int.bitSize + MagicData(M, a, s) + } + + // private[optimizer] for unit tests + private[optimizer] def computeUnsignedMagic[T](ad: T, negativeDivisor: Boolean)( + implicit int: UnsignedIntegral[T]): MagicData[T] = { + import int.mkNumericOps + + val two31 = int.one_<<(int.bitSize - 1) + + var add = 0 + + val t = two31 + (if (negativeDivisor) int.one else int.zero) + //val anc = t - int.one - (int.remainderUnsigned(t, ad)) // absolute value of nc + val anc = -int.one - int.remainderUnsigned(-ad, ad) + + var p = int.bitSize - 1 // init p + var q1 = int.divideUnsigned(two31, anc) // init q1 = 2**p / |nc| + var r1 = two31 - q1 * anc // init r1 = rem(2**p, |nc|) + var q2 = int.divideUnsigned(two31 - int.one, ad) // init q2 = (2**p - 1) / |d| + var r2 = (two31 - int.one) - q2 * ad // init r2 = rem(2**p - 1, |d|) + + while ({ + // do + p += 1 + + if (int.compareUnsigned(r1, anc - r1) >= 0) { + q1 = int.times2(q1) + int.one + r1 = int.times2(r1) - anc + } else { + q1 = int.times2(q1) + r1 = int.times2(r1) + } + + if (int.compareUnsigned(r2 + int.one, ad - r2) >= 0) { + if (int.compareUnsigned(q2, two31 - int.one) >= 0) + add = 1 + q2 = int.times2(q2) + int.one + r2 = int.times2(r2) + int.one - ad + } else { + if (int.compareUnsigned(q2, two31) >= 0) + add = 1 + q2 = int.times2(q2) + r2 = int.times2(r2) + int.one + } + + val delta = ad - int.one - r2 + // while + ((p < 2 * int.bitSize) && (int.compareUnsigned(q1, delta) < 0 || (q1 == delta && r1 == int.zero))) + }) {} + + val M = + if (negativeDivisor) throw new AssertionError("unreachable") + else q2 + int.one + val s = p - int.bitSize + MagicData(M, add, s) + } + + /** Like Integral[T], but with some unsigned operations that we need. */ + sealed trait UnsignedIntegral[T] extends Integral[T] { + /** Number of bits used to represent a value of type `T`. */ + val bitSize: Int + + def divideUnsigned(x: T, y: T): T + + def remainderUnsigned(x: T, y: T): T + + /** For example, for Int this is `31 - Integer.numberOfLeadingZeros(x)`. */ + def unsignedFloorLog2(x: T): Int + + def compareUnsigned(x: T, y: T): Int + + def isUnsignedPowerOf2(x: T): Boolean + + def one_<<(y: Int): T + + def times2(x: T): T + + // IR-related operations + + val tpe: Type + + def literal(x: T)(implicit pos: Position): Literal + + final def literalZero(implicit pos: Position): Literal = literal(zero) + + // scalastyle:off disallow.space.before.token + val Op_+ : BinaryOp.Code + val Op_- : BinaryOp.Code + val Op_* : BinaryOp.Code + val Op_& : BinaryOp.Code + val Op_>>> : BinaryOp.Code + val Op_>> : BinaryOp.Code + // scalastyle:on disallow.space.before.token + + def genMulSignedHi(x: T, y: VarRef, useRuntimeLong: Boolean)(implicit pos: Position): Tree + def genMulUnsignedHi(x: T, y: VarRef)(implicit pos: Position): Tree + } + + implicit object IntIsUnsignedIntegral + extends UnsignedIntegral[Int] + with Numeric.IntIsIntegral with Ordering.IntOrdering { + + val bitSize = 32 + + def divideUnsigned(x: Int, y: Int): Int = Integer.divideUnsigned(x, y) + + def remainderUnsigned(x: Int, y: Int): Int = Integer.remainderUnsigned(x, y) + + def unsignedFloorLog2(x: Int): Int = + 31 - Integer.numberOfLeadingZeros(x) + + def compareUnsigned(x: Int, y: Int): Int = + Integer.compareUnsigned(x, y) + + def isUnsignedPowerOf2(x: Int): Boolean = + (x & (x - 1)) == 0 && x != 0 + + def one_<<(y: Int): Int = + 1 << y + + def times2(x: Int): Int = + x << 1 + + // IR-related operations + + val tpe: Type = IntType + + def literal(x: Int)(implicit pos: Position): Literal = IntLiteral(x) + + // scalastyle:off disallow.space.before.token + val Op_+ : BinaryOp.Code = BinaryOp.Int_+ + val Op_- : BinaryOp.Code = BinaryOp.Int_- + val Op_* : BinaryOp.Code = BinaryOp.Int_* + val Op_& : BinaryOp.Code = BinaryOp.Int_& + val Op_>>> : BinaryOp.Code = BinaryOp.Int_>>> + val Op_>> : BinaryOp.Code = BinaryOp.Int_>> + // scalastyle:on disallow.space.before.token + + def genMulSignedHi(x: Int, y: VarRef, useRuntimeLong: Boolean)( + implicit pos: Position): Tree = { + /* (Math.multiplyFull(x, y) >>> 32).toInt + * + * Unfortunately, we cannot directly call that method, because it won't + * be available when we link a javalib < 1.20. + * + * Its user-land IR is easy enough to hard-code. However, we lose the + * handling as a RuntimeLOng intrinsic done by the optimizer. + * That is why we special-case useRuntimeLong at this level, to directly + * emit a call to the intrinsic implementation. + * + * On the flip side, that allows the ElementaryInterpreter to easily + * handle this code (for useRuntimeLong = false), so it's not lost. + */ + + if (useRuntimeLong) { + // RuntimeLong.multiplyFull(x, y).hi() + + val multiplyFullCall = ApplyStatic(ApplyFlags.empty, LongImpl.RuntimeLongClass, + MethodIdent(LongImpl.multiplyFull), List(IntLiteral(x), y))( + ClassType(LongImpl.RuntimeLongClass, nullable = true)) + + /* Use an explicit temp var to make sure the computation of `lo` can + * be dead-code-eliminated. For some reason, directly chaining the call + * to `hi()` leaves a record behind, which depends on `lo`. + */ + val tDef = tempVarDef(mulhiTempName, multiplyFullCall) + Block( + tDef, + Apply(ApplyFlags.empty, tDef.ref, MethodIdent(LongImpl.hi), Nil)(IntType) + ) + } else { + // ((x.toLong * y.toLong) >>> 32).toInt + UnaryOp( + UnaryOp.LongToInt, + BinaryOp( + BinaryOp.Long_>>>, + BinaryOp( + BinaryOp.Long_*, + LongLiteral(x.toLong), + UnaryOp(UnaryOp.IntToLong, y) + ), + IntLiteral(32) + ) + ) + } + } + + def genMulUnsignedHi(x: Int, y: VarRef)(implicit pos: Position): Tree = { + // ((Integer.toUnsignedLong(x) * Integer.toUnsignedLong(y)) >>> 32).toInt + UnaryOp( + UnaryOp.LongToInt, + BinaryOp( + BinaryOp.Long_>>>, + BinaryOp( + BinaryOp.Long_*, + LongLiteral(Integer.toUnsignedLong(x)), + UnaryOp(UnaryOp.UnsignedIntToLong, y) + ), + IntLiteral(32) + ) + ) + } + } + + implicit object LongIsUnsignedIntegral + extends UnsignedIntegral[Long] + with Numeric.LongIsIntegral with Ordering.LongOrdering { + + val bitSize = 64 + + def divideUnsigned(x: Long, y: Long): Long = JLong.divideUnsigned(x, y) + + def remainderUnsigned(x: Long, y: Long): Long = JLong.remainderUnsigned(x, y) + + def unsignedFloorLog2(x: Long): Int = + 63 - JLong.numberOfLeadingZeros(x) + + def compareUnsigned(x: Long, y: Long): Int = + JLong.compareUnsigned(x, y) + + def isUnsignedPowerOf2(x: Long): Boolean = + (x & (x - 1L)) == 0L && x != 0L + + def one_<<(y: Int): Long = + 1L << y + + def times2(x: Long): Long = + x << 1 + + // IR-related operations + + val tpe: Type = LongType + + def literal(x: Long)(implicit pos: Position): Literal = LongLiteral(x) + + // scalastyle:off disallow.space.before.token + val Op_+ : BinaryOp.Code = BinaryOp.Long_+ + val Op_- : BinaryOp.Code = BinaryOp.Long_- + val Op_* : BinaryOp.Code = BinaryOp.Long_* + val Op_& : BinaryOp.Code = BinaryOp.Long_& + val Op_>>> : BinaryOp.Code = BinaryOp.Long_>>> + val Op_>> : BinaryOp.Code = BinaryOp.Long_>> + // scalastyle:on disallow.space.before.token + + def genMulSignedHi(x: Long, y: VarRef, useRuntimeLong: Boolean)( + implicit pos: Position): Tree = { + /* Math.multiplyHigh(x, y) + * Unfortunately, we cannot directly call that method, because it won't + * be available when we link a javalib < 1.20. So we hard-code its IR. + * On the flip side, that allows the ElementaryInterpreter to easily + * handle this code, so it's not lost. + */ + genMulHiCommon(x, y, BinaryOp.Long_>>) + } + + def genMulUnsignedHi(x: Long, y: VarRef)(implicit pos: Position): Tree = { + /* Math.unsignedMultiplyHigh(x, y) + * Same remark as above. + */ + genMulHiCommon(x, y, BinaryOp.Long_>>>) + } + + private def genMulHiCommon(x: Long, y: VarRef, shiftOp: BinaryOp.Code)( + implicit pos: Position): Tree = { + /* In this code, >>> is unconditionally `Long_>>>`, but >> is `shiftOp`. + * + * val x0 = x & 0xffffffffL + * val x1 = x >> 32 + * val y0 = y & 0xffffffffL + * val y1 = y >> 32 + * val t = x1 * y0 + ((x0 * y0) >>> 32) + * x1 * y1 + (t >> 32) + (((t & 0xffffffffL) + x0 * y1) >> 32) + */ + + val x0 = LongLiteral(x & 0xffffffffL) + val x1 = LongLiteral(if (shiftOp == BinaryOp.Long_>>) x >> 32 else x >>> 32) + val lit32 = IntLiteral(32) + val litMask = LongLiteral(0xffffffffL) + + val y0Def = tempVarDef(y0Name, BinaryOp(BinaryOp.Long_&, litMask, y)) + val y0 = y0Def.ref + val y1Def = tempVarDef(y1Name, BinaryOp(shiftOp, y, lit32)) + val y1 = y1Def.ref + + val tDef = tempVarDef( + mulhiTempName, + BinaryOp( + BinaryOp.Long_+, + BinaryOp(BinaryOp.Long_*, x1, y0), + BinaryOp( + BinaryOp.Long_>>>, + BinaryOp(BinaryOp.Long_*, x0, y0), + lit32 + ) + ) + ) + val t = tDef.ref + + val result = BinaryOp( + BinaryOp.Long_+, + BinaryOp( + BinaryOp.Long_+, + // x1 * y1 + BinaryOp(BinaryOp.Long_*, x1, y1), + // t >> 32 + BinaryOp(shiftOp, t, lit32) + ), + // ((t & 0xffffffffL) + x0 * y1) >> 32 + BinaryOp( + shiftOp, + BinaryOp( + BinaryOp.Long_+, + BinaryOp(BinaryOp.Long_&, litMask, t), + BinaryOp(BinaryOp.Long_*, x0, y1) + ), + lit32 + ) + ) + + Block( + y0Def, + y1Def, + tDef, + result + ) + } + } +} 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 c0fd013511..f6b75e2e9f 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 @@ -158,6 +158,8 @@ private[optimizer] abstract class OptimizerCore( private val intrinsics = Intrinsics.buildIntrinsics(config.coreSpec.esFeatures, isWasm) + private lazy val integerDivisions = new IntegerDivisions(useRuntimeLong) + def optimize(thisType: Type, params: List[ParamDef], jsClassCaptures: List[ParamDef], resultType: Type, body: Tree, isNoArgCtor: Boolean): (List[ParamDef], Tree) = { @@ -3567,6 +3569,30 @@ private[optimizer] abstract class OptimizerCore( throw new AssertionError(s"failed to inline RuntimeLong method $methodName at $pos")) } + def isIntDivOp(op: BinaryOp.Code): Boolean = (op: @switch) match { + case BinaryOp.Int_/ | BinaryOp.Int_% | BinaryOp.Int_unsigned_/ | BinaryOp.Int_unsigned_% => + true + case _ => + false + } + + def isLongDivOp(op: BinaryOp.Code): Boolean = (op: @switch) match { + case BinaryOp.Long_/ | BinaryOp.Long_% | BinaryOp.Long_unsigned_/ | BinaryOp.Long_unsigned_% => + true + case _ => + false + } + + def expandOptimizedDivision(arg: PreTransform, body: Tree): TailRec[Tree] = { + val argBinding = Binding(LocalIdent(IntegerDivisions.NumeratorArgName), + NoOriginalName, arg.tpe.base, mutable = false, arg) + + withBinding(argBinding) { (bodyScope, cont1) => + implicit val scope = bodyScope + pretransformExpr(body)(cont1) + } (cont) (scope.withEnv(OptEnv.Empty)) + } + pretrans match { case PreTransUnaryOp(op, arg) if useRuntimeLong => import UnaryOp._ @@ -3605,6 +3631,14 @@ private[optimizer] abstract class OptimizerCore( cont(pretrans) } + case PreTransBinaryOp(op, lhs, PreTransLit(IntLiteral(r))) if isIntDivOp(op) => + val optimizedBody = integerDivisions.makeOptimizedDivision(op, r) + expandOptimizedDivision(lhs, optimizedBody) + + case PreTransBinaryOp(op, lhs, PreTransLit(LongLiteral(r))) if isLongDivOp(op) => + val optimizedBody = integerDivisions.makeOptimizedDivision(op, r) + expandOptimizedDivision(lhs, optimizedBody) + case PreTransBinaryOp(op, lhs, rhs) if useRuntimeLong => import BinaryOp._ diff --git a/linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/ElementaryInterpreter.scala b/linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/ElementaryInterpreter.scala new file mode 100644 index 0000000000..3bfe76f17f --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/ElementaryInterpreter.scala @@ -0,0 +1,157 @@ +/* + * 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.optimizer + +import scala.annotation.switch + +import java.lang.{Long => JLong} + +import org.scalajs.ir.Position +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees._ + +/** An IR interpreter for elementary operations, that we use for fuzzing. */ +object ElementaryInterpreter { + private implicit val pos: Position = Position.NoPosition + + def eval(tree: Tree, args: (LocalName, Literal)*): Literal = + eval(tree)(Env.fromArgs(args.toMap)) + + private def eval(tree: Tree)(implicit env: Env): Literal = tree match { + case lit: Literal => + lit + + case VarRef(name) => + env.local(name) + + case Block(stats) => + val exprEnv = stats.init.foldLeft(env)((env, stat) => evalBlockStat(stat)(env)) + eval(stats.last)(exprEnv) + + case tree: VarDef => + evalBlockStat(tree) + Undefined() + + case UnaryOp(op, arg) => + evalUnaryOp(op, eval(arg)) + + case BinaryOp(op, lhs, rhs) => + evalBinaryOp(op, eval(lhs), eval(rhs)) + + case _ => + throw new UnsupportedOperationException( + s"Unsupported tree in the ElementaryInterpreter, " + + s"of class ${tree.getClass().getSimpleName()}:\n${tree.show}") + } + + private def evalBlockStat(stat: Tree)(implicit env: Env): Env = { + stat match { + case VarDef(LocalIdent(name), _, _, mutable, rhs) => + if (mutable) { + throw new UnsupportedOperationException( + s"Unsupported mutable local var in the ElementaryInterpreter:\n${stat.show}") + } + env.withLocal(name, eval(rhs)) + + case _ => + // dead code, since we don't support any side effect, but why not + eval(stat) + env + } + } + + private def evalUnaryOp(op: UnaryOp.Code, arg: Literal): Literal = { + import UnaryOp._ + + (op: @switch) match { + case IntToLong => long(asInt(arg).toLong) + case LongToInt => int(asLong(arg).toInt) + case UnsignedIntToLong => long(Integer.toUnsignedLong(asInt(arg))) + + case _ => + throw new UnsupportedOperationException( + s"Unsupported unary op in the ElementaryInterpreter\n" + + s"${UnaryOp(op, arg).show}") + } + } + + private def evalBinaryOp(op: UnaryOp.Code, lhs: Literal, rhs: Literal): Literal = { + import BinaryOp._ + + (op: @switch) match { + case Int_+ => int(asInt(lhs) + asInt(rhs)) + case Int_- => int(asInt(lhs) - asInt(rhs)) + case Int_* => int(asInt(lhs) * asInt(rhs)) + case Int_/ => int(asInt(lhs) / asInt(rhs)) + case Int_% => int(asInt(lhs) % asInt(rhs)) + + case Int_| => int(asInt(lhs) | asInt(rhs)) + case Int_& => int(asInt(lhs) & asInt(rhs)) + case Int_^ => int(asInt(lhs) ^ asInt(rhs)) + case Int_<< => int(asInt(lhs) << asInt(rhs)) + case Int_>>> => int(asInt(lhs) >>> asInt(rhs)) + case Int_>> => int(asInt(lhs) >> asInt(rhs)) + + case Long_+ => long(asLong(lhs) + asLong(rhs)) + case Long_- => long(asLong(lhs) - asLong(rhs)) + case Long_* => long(asLong(lhs) * asLong(rhs)) + case Long_/ => long(asLong(lhs) / asLong(rhs)) + case Long_% => long(asLong(lhs) % asLong(rhs)) + + case Long_| => long(asLong(lhs) | asLong(rhs)) + case Long_& => long(asLong(lhs) & asLong(rhs)) + case Long_^ => long(asLong(lhs) ^ asLong(rhs)) + case Long_<< => long(asLong(lhs) << asInt(rhs)) + case Long_>>> => long(asLong(lhs) >>> asInt(rhs)) + case Long_>> => long(asLong(lhs) >> asInt(rhs)) + + case Int_unsigned_/ => int(Integer.divideUnsigned(asInt(lhs), asInt(rhs))) + case Int_unsigned_% => int(Integer.remainderUnsigned(asInt(lhs), asInt(rhs))) + + case Long_unsigned_/ => long(JLong.divideUnsigned(asLong(lhs), asLong(rhs))) + case Long_unsigned_% => long(JLong.remainderUnsigned(asLong(lhs), asLong(rhs))) + + case _ => + throw new UnsupportedOperationException( + s"Unsupported binary op in the ElementaryInterpreter\n" + + s"${BinaryOp(op, lhs, rhs).show}") + } + } + + private def int(value: Int): IntLiteral = IntLiteral(value) + + private def asInt(value: Literal): Int = { + val IntLiteral(intValue) = value: @unchecked + intValue + } + + private def long(value: Long): LongLiteral = LongLiteral(value) + + private def asLong(value: Literal): Long = { + val LongLiteral(longValue) = value: @unchecked + longValue + } + + private class Env(locals: Map[LocalName, Literal]) { + def local(name: LocalName): Literal = + locals(name) + + def withLocal(name: LocalName, value: Literal): Env = + new Env(locals + (name -> value)) + } + + private object Env { + def fromArgs(args: Map[LocalName, Literal]): Env = + new Env(args) + } +} diff --git a/linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisionsTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisionsTest.scala new file mode 100644 index 0000000000..9e07359865 --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/frontend/optimizer/IntegerDivisionsTest.scala @@ -0,0 +1,591 @@ +/* + * 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.optimizer + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ + +import org.scalajs.linker.testutils.TestIRBuilder._ +import org.scalajs.linker.testutils.TreeDSL._ + +import IntegerDivisions._ + +class IntegerDivisionsTest { + // We use many binary ops in these tests + import BinaryOp._ + + private val inum = VarRef(NumeratorArgName)(IntType) + private val lnum = VarRef(NumeratorArgName)(LongType) + + // ---------------------------------------------------- + // --- Unit tests for the computation of magic data --- + // ---------------------------------------------------- + + @Test def testComputeSignedMagicInt(): Unit = { + def test(expectedM: Int, expectedAdd: Int, expectedShift: Int, divisor: Int): Unit = { + assertEquals( + divisor.toString(), + MagicData(expectedM, expectedAdd, expectedShift), + computeSignedMagic(Math.abs(divisor), divisor < 0)) + } + + // From section 10-5 + + test(0x6DB6DB6D, -1, 2, -7) + + // Test cases from Hacker's Delight, 10-14, Table 10-1, Signed column + + test(0x99999999, 0, 1, -5) + test(0x55555555, -1, 1, -3) + test(0x55555556, 0, 0, 3) + test(0x66666667, 0, 1, 5) + test(0x2AAAAAAB, 0, 0, 6) + test(0x92492493, 1, 2, 7) + test(0x38E38E39, 0, 1, 9) + test(0x66666667, 0, 2, 10) + test(0x2E8BA2E9, 0, 1, 11) + test(0x2AAAAAAB, 0, 1, 12) + test(0x51EB851F, 0, 3, 25) + test(0x10624DD3, 0, 3, 125) + test(0x68DB8BAD, 0, 8, 625) + + // For completeness, but we never use the results of this function for powers of 2 + for (k <- 1 to 30) { + test(0x7fffffff, -1, k - 1, -(1 << k)) + test(0x80000001, 1, k - 1, 1 << k) + } + } + + @Test def testComputeUnsignedMagicInt(): Unit = { + def test(expectedM: Int, expectedAdd: Int, expectedShift: Int, divisor: Int): Unit = { + assertEquals( + divisor.toString(), + MagicData(expectedM, expectedAdd, expectedShift), + computeUnsignedMagic(divisor, negativeDivisor = false)) + } + + // Test cases from Hacker's Delight, 10-14, Table 10-1, Unsigned column + + test(0xAAAAAAAB, 0, 1, 3) + test(0xCCCCCCCD, 0, 2, 5) + test(0xAAAAAAAB, 0, 2, 6) + test(0x24924925, 1, 3, 7) + test(0x38E38E39, 0, 1, 9) + test(0xCCCCCCCD, 0, 3, 10) + test(0xBA2E8BA3, 0, 3, 11) + test(0xAAAAAAAB, 0, 3, 12) + test(0x51EB851F, 0, 3, 25) + test(0x10624DD3, 0, 3, 125) + test(0xD1B71759, 0, 9, 625) + + // For completeness, but we never use the results of this function for powers of 2 + for (k <- 1 to 31) + test(1 << (32 - k), 0, 0, 1 << k) + } + + @Test def testComputeSignedMagicLong(): Unit = { + def test(expectedM: Long, expectedAdd: Int, expectedShift: Int, divisor: Long): Unit = { + assertEquals( + divisor.toString(), + MagicData(expectedM, expectedAdd, expectedShift), + computeSignedMagic(Math.abs(divisor), divisor < 0L)) + } + + // Test cases from Hacker's Delight, 10-14, Table 10-2, Signed column + + test(0x9999999999999999L, 0, 1, -5L) + test(0x5555555555555555L, -1, 1, -3L) + test(0x5555555555555556L, 0, 0, 3L) + test(0x6666666666666667L, 0, 1, 5L) + test(0x2AAAAAAAAAAAAAABL, 0, 0, 6L) + test(0x4924924924924925L, 0, 1, 7L) + test(0x1C71C71C71C71C72L, 0, 0, 9L) + test(0x6666666666666667L, 0, 2, 10L) + test(0x2E8BA2E8BA2E8BA3L, 0, 1, 11L) + test(0x2AAAAAAAAAAAAAABL, 0, 1, 12L) + test(0xA3D70A3D70A3D70BL, 1, 4, 25L) + test(0x20C49BA5E353F7CFL, 0, 4, 125L) + test(0x346DC5D63886594BL, 0, 7, 625L) + + // For completeness, but we never use the results of this function for powers of 2 + for (k <- 1 to 62) { + test(0x7fffffffffffffffL, -1, k - 1, -(1L << k)) + test(0x8000000000000001L, 1, k - 1, 1L << k) + } + } + + @Test def testComputeUnsignedMagicLong(): Unit = { + def test(expectedM: Long, expectedAdd: Int, expectedShift: Int, divisor: Long): Unit = { + assertEquals( + divisor.toString(), + MagicData(expectedM, expectedAdd, expectedShift), + computeUnsignedMagic(divisor, negativeDivisor = false)) + } + + // Test cases from Hacker's Delight, 10-14, Table 10-2, Unsigned column + + test(0xAAAAAAAAAAAAAAABL, 0, 1, 3L) + test(0xCCCCCCCCCCCCCCCDL, 0, 2, 5L) + test(0xAAAAAAAAAAAAAAABL, 0, 2, 6L) + test(0x2492492492492493L, 1, 3, 7L) + test(0xE38E38E38E38E38FL, 0, 3, 9L) + test(0xCCCCCCCCCCCCCCCDL, 0, 3, 10L) + test(0x2E8BA2E8BA2E8BA3L, 0, 1, 11L) + test(0xAAAAAAAAAAAAAAABL, 0, 3, 12L) + test(0x47AE147AE147AE15L, 1, 5, 25L) + test(0x0624DD2F1A9FBE77L, 1, 7, 125L) + test(0x346DC5D63886594BL, 0, 7, 625L) + + // For completeness, but we never use the results of this function for powers of 2 + for (k <- 1 to 63) + test(1L << (64 - k), 0, 0, 1L << k) + } + + // ----------------------------------------------------------------- + // --- Testing the specific shape of trees for selected divisors --- + // ----------------------------------------------------------------- + + /* Note that we do not test specific shapes for Long operations. + * Since the same, generic logic is used for both Int and Long, it is not + * worth it. Testing Long operations would require a different set of TreeDSL + * helpers for operations on Longs, which would be inconvenient. + */ + + private def testSpecificShape(op: BinaryOp.Code, divisor: Int)(expectedTree: Tree): Unit = { + val actual = new IntegerDivisions(useRuntimeLong = false) + .makeOptimizedDivision(op, divisor) + if (actual != expectedTree) + fail(s"Expected:\n${expectedTree.show}\nbut got\n${actual.show}") + } + + @Test def testSpecificShape_Int_/(): Unit = { + def test(divisor: Int)(expectedTree: Tree): Unit = + testSpecificShape(Int_/, divisor)(expectedTree) + + test(1) { + inum + } + + test(-1) { + -inum + } + + test(2) { + (inum + ((inum >> 0) >>> 31)) >> 1 + } + + test(-2) { + -((inum + ((inum >> 0) >>> 31)) >> 1) + } + + test(8) { + (inum + ((inum >> 2) >>> 29)) >> 3 + } + + test(-8) { + -((inum + ((inum >> 2) >>> 29)) >> 3) + } + + test(Int.MinValue) { + -((inum + ((inum >> 30) >>> 1)) >> 31) + } + + test(3) { + (IntIsUnsignedIntegral.genMulSignedHi(0x55555556, inum, false) >> 0) + (inum >>> 31) + } + + test(5) { + (IntIsUnsignedIntegral.genMulSignedHi(0x66666667, inum, false) >> 1) + (inum >>> 31) + } + + // TODO Optimize according to section 10-5 + test(-5) { + -((IntIsUnsignedIntegral.genMulSignedHi(0x66666667, inum, false) >> 1) + (inum >>> 31)) + } + + test(7) { + ((IntIsUnsignedIntegral.genMulSignedHi(0x92492493, inum, false) + inum) >> 2) + (inum >>> 31) + } + + // TODO Optimize according to section 10-5 + test(-7) { + -(((IntIsUnsignedIntegral.genMulSignedHi(0x92492493, inum, false) + inum) >> 2) + (inum >>> 31)) + } + } + + @Test def testSpecificShape_Int_%(): Unit = { + def test(divisor: Int)(expectedTree: Tree): Unit = + testSpecificShape(Int_%, divisor)(expectedTree) + + test(1) { + int(0) + } + + test(-1) { + int(0) + } + + test(2) { + val t = VarDef(LocalIdent(tName), NON, IntType, mutable = false, + (inum >> 0) >>> 31) + Block( + t, + ((inum + t.ref) & 1) - t.ref + ) + } + + test(-2) { + val t = VarDef(LocalIdent(tName), NON, IntType, mutable = false, + (inum >> 0) >>> 31) + Block( + t, + ((inum + t.ref) & 1) - t.ref + ) + } + + test(8) { + val t = VarDef(LocalIdent(tName), NON, IntType, mutable = false, + (inum >> 2) >>> 29) + Block( + t, + ((inum + t.ref) & 7) - t.ref + ) + } + + test(-8) { + val t = VarDef(LocalIdent(tName), NON, IntType, mutable = false, + (inum >> 2) >>> 29) + Block( + t, + ((inum + t.ref) & 7) - t.ref + ) + } + + test(Int.MinValue) { + val t = VarDef(LocalIdent(tName), NON, IntType, mutable = false, + (inum >> 30) >>> 1) + Block( + t, + ((inum + t.ref) & Int.MaxValue) - t.ref + ) + } + + test(3) { + inum - int(3) * { + (IntIsUnsignedIntegral.genMulSignedHi(0x55555556, inum, false) >> 0) + (inum >>> 31) + } + } + + test(5) { + inum - int(5) * { + (IntIsUnsignedIntegral.genMulSignedHi(0x66666667, inum, false) >> 1) + (inum >>> 31) + } + } + + // TODO Optimize according to section 10-5 + test(-5) { + inum - int(-5) * { + -((IntIsUnsignedIntegral.genMulSignedHi(0x66666667, inum, false) >> 1) + (inum >>> 31)) + } + } + + test(7) { + inum - int(7) * { + ((IntIsUnsignedIntegral.genMulSignedHi(0x92492493, inum, false) + inum) >> 2) + (inum >>> 31) + } + } + + // TODO Optimize according to section 10-5 + test(-7) { + inum - int(-7) * { + -(((IntIsUnsignedIntegral.genMulSignedHi(0x92492493, inum, false) + inum) >> 2) + (inum >>> 31)) + } + } + } + + @Test def testSpecificShape_Int_unsigned_/(): Unit = { + def test(divisor: Int)(expectedTree: Tree): Unit = + testSpecificShape(Int_unsigned_/, divisor)(expectedTree) + + test(1) { + inum + } + + test(2) { + inum >>> 1 + } + + test(8) { + inum >>> 3 + } + + test(Int.MinValue) { + inum >>> 31 + } + + test(3) { + IntIsUnsignedIntegral.genMulUnsignedHi(0xAAAAAAAB, inum) >>> 1 + } + + test(5) { + IntIsUnsignedIntegral.genMulUnsignedHi(0xCCCCCCCD, inum) >>> 2 + } + + test(-7) { + IntIsUnsignedIntegral.genMulUnsignedHi(0x20000001, inum) >>> 29 + } + + test(-5) { + IntIsUnsignedIntegral.genMulUnsignedHi(0x80000003, inum) >>> 31 + } + + test(-1) { + IntIsUnsignedIntegral.genMulUnsignedHi(0x80000001, inum) >>> 31 + } + + test(7) { + val hi = VarDef(LocalIdent(hiName), NON, IntType, mutable = false, + IntIsUnsignedIntegral.genMulUnsignedHi(0x24924925, inum)) + Block( + hi, + (((inum - hi.ref) >>> 1) + hi.ref) >>> 2 + ) + } + + test(-8) { + val hi = VarDef(LocalIdent(hiName), NON, IntType, mutable = false, + IntIsUnsignedIntegral.genMulUnsignedHi(0x00000009, inum)) + Block( + hi, + (((inum - hi.ref) >>> 1) + hi.ref) >>> 31 + ) + } + } + + @Test def testSpecificShape_Int_unsigned_%(): Unit = { + def test(divisor: Int)(expectedTree: Tree): Unit = + testSpecificShape(Int_unsigned_%, divisor)(expectedTree) + + test(1) { + int(0) + } + + test(2) { + inum & 1 + } + + test(8) { + inum & 7 + } + + test(Int.MinValue) { + inum & Int.MaxValue + } + + test(3) { + inum - int(3) * (IntIsUnsignedIntegral.genMulUnsignedHi(0xAAAAAAAB, inum) >>> 1) + } + + test(5) { + inum - int(5) * (IntIsUnsignedIntegral.genMulUnsignedHi(0xCCCCCCCD, inum) >>> 2) + } + + test(-7) { + inum - int(-7) * (IntIsUnsignedIntegral.genMulUnsignedHi(0x20000001, inum) >>> 29) + } + + test(-5) { + inum - int(-5) * (IntIsUnsignedIntegral.genMulUnsignedHi(0x80000003, inum) >>> 31) + } + + test(-1) { + inum - int(-1) * (IntIsUnsignedIntegral.genMulUnsignedHi(0x80000001, inum) >>> 31) + } + + test(7) { + inum - int(7) * { + val hi = VarDef(LocalIdent(hiName), NON, IntType, mutable = false, + IntIsUnsignedIntegral.genMulUnsignedHi(0x24924925, inum)) + Block( + hi, + (((inum - hi.ref) >>> 1) + hi.ref) >>> 2 + ) + } + } + + test(-8) { + inum - int(-8) * { + val hi = VarDef(LocalIdent(hiName), NON, IntType, mutable = false, + IntIsUnsignedIntegral.genMulUnsignedHi(0x00000009, inum)) + Block( + hi, + (((inum - hi.ref) >>> 1) + hi.ref) >>> 31 + ) + } + } + } + + // --------------------------------------------------------------------------------------------------- + // --- Fuzz tests: test that the optimizer trees evaluate to the same result as the original trees --- + // --------------------------------------------------------------------------------------------------- + + @Test @noinline def fuzzTestInt_/(): Unit = fuzzTestIntOp(BinaryOp.Int_/) + @Test @noinline def fuzzTestInt_%(): Unit = fuzzTestIntOp(BinaryOp.Int_%) + @Test @noinline def fuzzTestInt_unsigned_/(): Unit = fuzzTestIntOp(BinaryOp.Int_unsigned_/) + @Test @noinline def fuzzTestInt_unsigned_%(): Unit = fuzzTestIntOp(BinaryOp.Int_unsigned_%) + + @Test @noinline def fuzzTestLong_/(): Unit = fuzzTestLongOp(BinaryOp.Long_/) + @Test @noinline def fuzzTestLong_%(): Unit = fuzzTestLongOp(BinaryOp.Long_%) + @Test @noinline def fuzzTestLong_unsigned_/(): Unit = fuzzTestLongOp(BinaryOp.Long_unsigned_/) + @Test @noinline def fuzzTestLong_unsigned_%(): Unit = fuzzTestLongOp(BinaryOp.Long_unsigned_%) + + private val FuzzTestDivisorsSeed = 4075846924374047794L + private val FuzzTestNumeratorsSeed = 8312049509154985420L + private val FuzzTestDivisorsRounds = 10000 + private val FuzzTestNumeratorsRounds = 100 + // There will be FuzzTestDivisorsRound * FuzzTestNumeratorsRound inner iterations! + + private def fuzzTestIntOp(op: BinaryOp.Code): Unit = { + def testDivisor(divisor: Int): Unit = + fuzzTestIntDivisor(op, divisor) + + testDivisor(0) + for (small <- 1 to 13) { + testDivisor(small) + testDivisor(-small) + } + for (k <- 4 until 32) { // up to 2^3 are tested in small + testDivisor(1 << k) + testDivisor(-(1 << k)) + } + testDivisor(100) + testDivisor(1000) + testDivisor(Int.MaxValue - 1) + testDivisor(Int.MaxValue) + testDivisor(Int.MinValue) + testDivisor(Int.MinValue + 1) + + val rnd = new java.util.SplittableRandom(FuzzTestDivisorsSeed) + for (_ <- 0 until FuzzTestDivisorsRounds) + testDivisor(rnd.nextInt() >> rnd.nextInt(32)) + } + + private def fuzzTestLongOp(op: BinaryOp.Code): Unit = { + def testDivisor(divisor: Long): Unit = + fuzzTestLongDivisor(op, divisor) + + testDivisor(0L) + for (small <- 1 to 13) { + testDivisor(small.toLong) + testDivisor(-small.toLong) + } + for (k <- 4 until 64) { // up to 2^3 are tested in small + testDivisor(1L << k) + testDivisor(-(1L << k)) + } + testDivisor(100L) + testDivisor(1000L) + testDivisor(Long.MaxValue - 1L) + testDivisor(Long.MaxValue) + testDivisor(Long.MinValue) + testDivisor(Long.MinValue + 1L) + + val rnd = new java.util.SplittableRandom(FuzzTestDivisorsSeed) + for (_ <- 0 until FuzzTestDivisorsRounds) + testDivisor(rnd.nextLong() >> rnd.nextInt(64)) + } + + private def fuzzTestIntDivisor(op: BinaryOp.Code, divisor: Int): Unit = { + val reference = BinaryOp(op, inum, IntLiteral(divisor)) + val optimized = new IntegerDivisions(useRuntimeLong = false) + .makeOptimizedDivision(op, divisor) + + if (divisor == 0) { + val UnaryOp(UnaryOp.Throw, _) = optimized // meant as an assertion + } else { + def test(numValue: Int): Unit = + assertEvalEquals(reference, optimized, IntLiteral(numValue)) + + test(0) + for (small <- 1 to 13) { + test(small) + test(-small) + } + test(divisor - 1) + test(divisor) + test(divisor + 1) + test(-divisor + 1) + test(-divisor) + test(-divisor - 1) + test(Int.MaxValue - 1) + test(Int.MaxValue) + test(Int.MinValue) + test(Int.MinValue + 1) + + val rnd = new java.util.SplittableRandom(FuzzTestNumeratorsSeed) + for (_ <- 0 until FuzzTestNumeratorsRounds) + test(rnd.nextInt() >> rnd.nextInt(32)) + } + } + + private def fuzzTestLongDivisor(op: BinaryOp.Code, divisor: Long): Unit = { + val reference = BinaryOp(op, lnum, LongLiteral(divisor)) + val optimized = new IntegerDivisions(useRuntimeLong = false) + .makeOptimizedDivision(op, divisor) + + if (divisor == 0L) { + val UnaryOp(UnaryOp.Throw, _) = optimized // meant as an assertion + } else { + def test(numValue: Long): Unit = + assertEvalEquals(reference, optimized, LongLiteral(numValue)) + + test(0L) + for (small <- 1 to 13) { + test(small.toLong) + test(-small.toLong) + } + test(divisor - 1L) + test(divisor) + test(divisor + 1L) + test(-divisor + 1L) + test(-divisor) + test(-divisor - 1L) + test(Long.MaxValue - 1L) + test(Long.MaxValue) + test(Long.MinValue) + test(Long.MinValue + 1L) + + val rnd = new java.util.SplittableRandom(FuzzTestNumeratorsSeed) + for (_ <- 0 until FuzzTestNumeratorsRounds) + test(rnd.nextLong() >> rnd.nextInt(64)) + } + } + + private def assertEvalEquals(reference: Tree, optimized: Tree, numValue: Literal): Unit = { + val referenceResult = ElementaryInterpreter.eval(reference, NumeratorArgName -> numValue) + val optimizedResult = ElementaryInterpreter.eval(optimized, NumeratorArgName -> numValue) + + if (optimizedResult != referenceResult) { + fail( + s"Fuzz test failed\n" + + s"Expected ${referenceResult.show} but got ${optimizedResult.show}\n" + + s"Numerator: ${numValue.show}\n" + + s"Reference code was:\n${reference.show}\n" + + s"Optimized code was:\n${optimized.show}\n") + } + } +} diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TreeDSL.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TreeDSL.scala new file mode 100644 index 0000000000..45c31744ba --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TreeDSL.scala @@ -0,0 +1,40 @@ +/* + * 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.testutils + +import org.scalajs.ir.Position +import org.scalajs.ir.Trees._ + +object TreeDSL { + implicit class TreeOps(private val self: Tree) { + def +(that: Tree)(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_+, self, that) + def -(that: Tree)(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_-, self, that) + def *(that: Tree)(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_*, self, that) + + def unary_-(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_-, IntLiteral(0), self) + + def &(that: Int)(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_&, self, IntLiteral(that)) + + def <<(that: Int)(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_<<, self, IntLiteral(that)) + def >>>(that: Int)(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_>>>, self, IntLiteral(that)) + def >>(that: Int)(implicit pos: Position): Tree = + BinaryOp(BinaryOp.Int_>>, self, IntLiteral(that)) + } +} diff --git a/project/Build.scala b/project/Build.scala index 071d5016db..fef0bc715e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2053,34 +2053,34 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 625000 to 626000, + fastLink = 626000 to 627000, fullLink = 94000 to 95000, fastLinkGz = 75000 to 79000, fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 426000 to 427000, - fullLink = 283000 to 284000, + fastLink = 427000 to 428000, + fullLink = 284000 to 285000, fastLinkGz = 61000 to 62000, - fullLinkGz = 43000 to 44000, + fullLinkGz = 44000 to 45000, )) } case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 443000 to 444000, - fullLink = 90000 to 91000, - fastLinkGz = 57000 to 58000, + fastLink = 444000 to 445000, + fullLink = 91000 to 92000, + fastLinkGz = 58000 to 59000, fullLinkGz = 24000 to 25000, )) } else { Some(ExpectedSizes( - fastLink = 301000 to 302000, - fullLink = 259000 to 260000, - fastLinkGz = 47000 to 48000, - fullLinkGz = 42000 to 43000, + fastLink = 303000 to 304000, + fullLink = 261000 to 262000, + fastLinkGz = 48000 to 49000, + fullLinkGz = 43000 to 44000, )) }