Skip to content

Commit bd4a860

Browse files
committed
Optimizer: Use casts for RuntimeLong inlining
This allows us to enable the IRChecker.
1 parent ca3ac05 commit bd4a860

File tree

4 files changed

+93
-55
lines changed

4 files changed

+93
-55
lines changed

linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,6 @@ final class Refiner(config: CommonPhaseConfig, checkIR: Boolean) {
3838
new Analyzer(config, initial = false, checkIRFor, failOnError = true, irLoader)
3939
}
4040

41-
/* TODO: Remove this and replace with `checkIR` once the optimizer generates
42-
* well-typed IR with runtime longs.
43-
*/
44-
private val shouldRunIRChecker = {
45-
val optimizerUsesRuntimeLong =
46-
!config.coreSpec.esFeatures.allowBigIntsForLongs &&
47-
!config.coreSpec.targetIsWebAssembly
48-
checkIR && !optimizerUsesRuntimeLong
49-
}
50-
5141
def refine(classDefs: Seq[(ClassDef, Version)],
5242
moduleInitializers: List[ModuleInitializer],
5343
symbolRequirements: SymbolRequirement, logger: Logger)(
@@ -81,7 +71,7 @@ final class Refiner(config: CommonPhaseConfig, checkIR: Boolean) {
8171
linkedTopLevelExports.flatten.toList, moduleInitializers, globalInfo)
8272
}
8373

84-
if (shouldRunIRChecker) {
74+
if (checkIR) {
8575
logger.time("Refiner: Check IR") {
8676
val errorCount = IRChecker.check(linkTimeProperties, result, logger,
8777
CheckingPhase.Optimizer)

linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -263,29 +263,8 @@ private[optimizer] abstract class OptimizerCore(
263263

264264
private val isSubclassFun = isSubclass _
265265

266-
private def isSubtype(lhs: Type, rhs: Type): Boolean = {
267-
assert(lhs != VoidType)
268-
assert(rhs != VoidType)
269-
270-
Types.isSubtype(lhs, rhs)(isSubclassFun) || {
271-
(lhs, rhs) match {
272-
case (LongType, ClassType(LongImpl.RuntimeLongClass, _)) =>
273-
true
274-
case (ClassType(LongImpl.RuntimeLongClass, false), LongType) =>
275-
true
276-
case (ClassType(BoxedLongClass, lhsNullable),
277-
ClassType(LongImpl.RuntimeLongClass, rhsNullable)) =>
278-
rhsNullable || !lhsNullable
279-
280-
case (ClassType(LongImpl.RuntimeLongClass, lhsNullable),
281-
ClassType(BoxedLongClass, rhsNullable)) =>
282-
rhsNullable || !lhsNullable
283-
284-
case _ =>
285-
false
286-
}
287-
}
288-
}
266+
private def isSubtype(lhs: Type, rhs: Type): Boolean =
267+
Types.isSubtype(lhs, rhs)(isSubclassFun)
289268

290269
/** Transforms a statement.
291270
*
@@ -577,8 +556,16 @@ private[optimizer] abstract class OptimizerCore(
577556
case IsInstanceOf(expr, testType) =>
578557
trampoline {
579558
pretransformExpr(expr) { texpr =>
559+
val texprType = texpr.tpe.base.toNonNullable
560+
561+
// Note: Disregards nullability because we can optimize null-check only.
562+
val staticSubtype = {
563+
isSubtype(texprType, testType) ||
564+
(useRuntimeLong && isRTLong(testType) && isRTLong(texprType))
565+
}
566+
580567
val result = {
581-
if (isSubtype(texpr.tpe.base.toNonNullable, testType)) {
568+
if (staticSubtype) {
582569
if (texpr.tpe.isNullable)
583570
BinaryOp(BinaryOp.!==, finishTransformExpr(texpr), Null())
584571
else
@@ -762,10 +749,23 @@ private[optimizer] abstract class OptimizerCore(
762749
def addCaptureParam(newName: LocalName): LocalDef = {
763750
val newOriginalName = originalNameForFresh(paramName, originalName, newName)
764751

752+
val captureTpe = {
753+
/* Do not refine the capture type for longs:
754+
* The pretransform might be a stack allocated RuntimeLong.
755+
* We cannot (trivially) capture it in stack allocated form.
756+
* Therefore, we keep the primitive type and let finishTransformExpr
757+
* allocate a RuntimeLong.
758+
*
759+
* TODO: Improve this and allocate two capture params for lo/hi?
760+
*/
761+
if (useRuntimeLong && paramDef.ptpe == LongType) RefinedType(LongType)
762+
else tcaptureValue.tpe
763+
}
764+
765765
val replacement = ReplaceWithVarRef(newName, newSimpleState(Unused))
766-
val localDef = LocalDef(tcaptureValue.tpe, mutable, replacement)
766+
val localDef = LocalDef(captureTpe, mutable, replacement)
767767
val localIdent = LocalIdent(newName)(ident.pos)
768-
val newParamDef = ParamDef(localIdent, newOriginalName, tcaptureValue.tpe.base, mutable)(paramDef.pos)
768+
val newParamDef = ParamDef(localIdent, newOriginalName, captureTpe.base, mutable)(paramDef.pos)
769769

770770
/* Note that the binding will never create a fresh name for a
771771
* ReplaceWithVarRef. So this will not put our name alignment at risk.
@@ -1297,12 +1297,22 @@ private[optimizer] abstract class OptimizerCore(
12971297
}
12981298

12991299
if (lhsStructure.className == LongImpl.RuntimeLongClass && trhs.tpe.base == LongType) {
1300-
/* The lhs is a stack-allocated RuntimeLong, but the rhs is a
1301-
* primitive Long. We expand the primitive Long into a new
1302-
* stack-allocated RuntimeLong so that we do not need to cancel.
1303-
*/
1304-
expandLongValue(trhs) { expandedRhs =>
1305-
buildInner(expandedRhs)
1300+
// The lhs is a stack-allocated RuntimeLong, the rhs is *typed* as primitive long.
1301+
1302+
trhs match {
1303+
case PreTransCast(trhs: PreTransRecordTree, _) =>
1304+
/* The rhs is also a stack allocated Long but was cast back to
1305+
* a primitive Long (due to method inlining). Remove the cast.
1306+
*/
1307+
buildInner(trhs)
1308+
1309+
case _ =>
1310+
/* The rhs is a primitive Long. We expand the primitive Long into
1311+
* a new stack-allocated RuntimeLong so that we do not need to cancel.
1312+
*/
1313+
expandLongValue(trhs) { expandedRhs =>
1314+
buildInner(expandedRhs)
1315+
}
13061316
}
13071317
} else {
13081318
buildInner(trhs)
@@ -5291,7 +5301,16 @@ private[optimizer] abstract class OptimizerCore(
52915301
def mayRequireUnboxing: Boolean =
52925302
arg.tpe.isNullable && tpe.isInstanceOf[PrimType]
52935303

5294-
if (semantics.asInstanceOfs == CheckedBehavior.Unchecked && !mayRequireUnboxing)
5304+
/* In methods on RuntimeLong, we often asInstanceOf Long to RuntimeLong and
5305+
* vice versa. We know that these are the same at runtime, so we lower to casts.
5306+
*/
5307+
val castForRTLong: Boolean = useRuntimeLong && {
5308+
val vtpe = arg.tpe.base
5309+
(!vtpe.isNullable || tpe.isNullable) &&
5310+
isRTLong(arg.tpe.base) && isRTLong(tpe)
5311+
}
5312+
5313+
if (semantics.asInstanceOfs == CheckedBehavior.Unchecked && !mayRequireUnboxing || castForRTLong)
52955314
foldCast(arg, tpe)
52965315
else if (isSubtype(arg.tpe.base, tpe))
52975316
arg
@@ -5825,6 +5844,16 @@ private[optimizer] abstract class OptimizerCore(
58255844
else upperBound
58265845
}
58275846

5847+
/** Whether the given type is a RuntimeLong long at runtime.
5848+
*
5849+
* Assumes useRuntimeLong.
5850+
*/
5851+
private def isRTLong(tpe: Type) = tpe match {
5852+
case LongType => true
5853+
case ClassType(LongImpl.RuntimeLongClass | BoxedLongClass, _) => true
5854+
case _ => false
5855+
}
5856+
58285857
/** Trampolines a pretransform */
58295858
private def trampoline(tailrec: => TailRec[Tree]): Tree = {
58305859
// scalastyle:off return
@@ -6687,8 +6716,8 @@ private[optimizer] object OptimizerCore {
66876716
private def createNewLong(lo: Tree, hi: Tree)(
66886717
implicit pos: Position): Tree = {
66896718

6690-
New(LongImpl.RuntimeLongClass, MethodIdent(LongImpl.initFromParts),
6691-
List(lo, hi))
6719+
makeCast(New(LongImpl.RuntimeLongClass, MethodIdent(LongImpl.initFromParts),
6720+
List(lo, hi)), LongType)
66926721
}
66936722

66946723
/** Tests whether `x + y` is valid without falling out of range. */

linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -490,18 +490,10 @@ object IRCheckerTest {
490490
moduleInitializers: List[ModuleInitializer],
491491
logger: Logger, postOptimizer: Boolean)(
492492
implicit ec: ExecutionContext): Future[Unit] = {
493-
val baseConfig = StandardConfig()
493+
val config = StandardConfig()
494494
.withCheckIR(true)
495495
.withOptimizer(false)
496496

497-
val config = {
498-
/* Disable RuntimeLongs to workaround the Refiner disabling IRChecks in this case.
499-
* TODO: Remove once we run IRChecks post optimizer all the time.
500-
*/
501-
if (postOptimizer) baseConfig.withESFeatures(_.withAllowBigIntsForLongs(true))
502-
else baseConfig
503-
}
504-
505497
val noSymbolRequirements = SymbolRequirement
506498
.factory("IRCheckerTest")
507499
.none()

linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,33 @@ class OptimizerTest {
489489
}
490490
}
491491

492+
@Test
493+
def testLongCaptures(): AsyncResult = await {
494+
val calc = m("calc", Nil, LongRef)
495+
496+
val classDefs = Seq(
497+
classDef(
498+
MainTestClassName,
499+
kind = ClassKind.Class,
500+
superClass = Some(ObjectClass),
501+
methods = List(
502+
// @noinline static def calc(): Long = 1L
503+
MethodDef(EMF.withNamespace(PublicStatic), calc, NON, Nil,
504+
LongType, Some(LongLiteral(1)))(EOH.withNoinline(true), UNV),
505+
mainMethodDef(Block(
506+
VarDef("x", NON, LongType, mutable = false,
507+
ApplyStatic(EAF, MainTestClassName, calc, Nil)(LongType)),
508+
consoleLog(Closure(ClosureFlags.arrow, List(paramDef("y", LongType)),
509+
Nil, None, AnyType, VarRef("y")(LongType), List(VarRef("x")(LongType))))
510+
))
511+
)
512+
)
513+
)
514+
515+
// Check it doesn't fail IRChecking.
516+
linkToModuleSet(classDefs, MainTestModuleInitializers)
517+
}
518+
492519
private def commonClassDefsForFieldRemovalTests(classInline: Boolean,
493520
witnessMutable: Boolean): Seq[ClassDef] = {
494521
val methodName = m("method", Nil, I)

0 commit comments

Comments
 (0)