Skip to content

Commit 3cfbb6d

Browse files
authored
Merge pull request #5354 from dotty-staging/fix-5340
Improve implementation of local lazy vals and synchronized blocks
2 parents b261160 + 3d33bae commit 3cfbb6d

File tree

9 files changed

+162
-91
lines changed

9 files changed

+162
-91
lines changed

compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class DottyBackendInterface(outputDirectory: AbstractFile, val superCallsMap: Ma
116116
val Throwable_Type: Type = defn.ThrowableType
117117
val Object_isInstanceOf: Symbol = defn.Any_isInstanceOf
118118
val Object_asInstanceOf: Symbol = defn.Any_asInstanceOf
119+
val Object_synchronized: Symbol = defn.Object_synchronized
119120
val Object_equals: Symbol = defn.Any_equals
120121
val ArrayClass: Symbol = defn.ArrayClass
121122
val UnitClass: Symbol = defn.UnitClass

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -897,10 +897,25 @@ class Definitions {
897897
// ----- Symbol sets ---------------------------------------------------
898898

899899
lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0)
900-
val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun[Array[Symbol]](implicit ctx => AbstractFunctionType.map(_.symbol.asClass))
900+
val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(implicit ctx => AbstractFunctionType.map(_.symbol.asClass))
901901
def AbstractFunctionClass(n: Int)(implicit ctx: Context): Symbol = AbstractFunctionClassPerRun()(ctx)(n)
902902
private lazy val ImplementedFunctionType = mkArityArray("scala.Function", MaxImplementedFunctionArity, 0)
903-
def FunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun[Array[Symbol]](implicit ctx => ImplementedFunctionType.map(_.symbol.asClass))
903+
def FunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(implicit ctx => ImplementedFunctionType.map(_.symbol.asClass))
904+
905+
val LazyHolder: PerRun[Map[Symbol, Symbol]] = new PerRun({ implicit ctx =>
906+
def holderImpl(holderType: String) = ctx.requiredClass("scala.runtime." + holderType)
907+
Map[Symbol, Symbol](
908+
IntClass -> holderImpl("LazyInt"),
909+
LongClass -> holderImpl("LazyLong"),
910+
BooleanClass -> holderImpl("LazyBoolean"),
911+
FloatClass -> holderImpl("LazyFloat"),
912+
DoubleClass -> holderImpl("LazyDouble"),
913+
ByteClass -> holderImpl("LazyByte"),
914+
CharClass -> holderImpl("LazyChar"),
915+
ShortClass -> holderImpl("LazyShort")
916+
)
917+
.withDefaultValue(holderImpl("LazyRef"))
918+
})
904919

905920
lazy val TupleType: Array[TypeRef] = mkArityArray("scala.Tuple", MaxTupleArity, 1)
906921

@@ -1059,7 +1074,7 @@ class Definitions {
10591074
lazy val NoInitClasses: Set[Symbol] = NotRuntimeClasses + FunctionXXLClass
10601075

10611076
def isPolymorphicAfterErasure(sym: Symbol): Boolean =
1062-
(sym eq Any_isInstanceOf) || (sym eq Any_asInstanceOf)
1077+
(sym eq Any_isInstanceOf) || (sym eq Any_asInstanceOf) || (sym eq Object_synchronized)
10631078

10641079
def isTupleType(tp: Type)(implicit ctx: Context): Boolean = {
10651080
val arity = tp.dealias.argInfos.length

compiler/src/dotty/tools/dotc/transform/LazyVals.scala

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -129,50 +129,56 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer {
129129
Thicket(field, getter)
130130
}
131131

132-
/** Replace a local lazy val inside a method,
133-
* with a LazyHolder from
134-
* dotty.runtime(eg dotty.runtime.LazyInt)
135-
*/
132+
/** Desugar a local `lazy val x: Int = <RHS>` into:
133+
*
134+
* ```
135+
* val x$lzy = new scala.runtime.LazyInt()
136+
*
137+
* def x$lzycompute(): Int = x$lzy.synchronized {
138+
* if (x$lzy.initialized()) x$lzy.value()
139+
* else x$lzy.initialize(<RHS>)
140+
* // TODO: Implement Unit-typed lazy val optimization described below
141+
* // for a Unit-typed lazy val, this becomes `{ rhs ; x$lzy.initialize() }`
142+
* // to avoid passing around BoxedUnit
143+
* }
144+
*
145+
* def x(): Int = if (x$lzy.initialized()) x$lzy.value() else x$lzycompute()
146+
* ```
147+
*/
136148
def transformLocalDef(x: ValOrDefDef)(implicit ctx: Context): Thicket = {
137-
val valueInitter = x.rhs
138-
val xname = x.name.asTermName
139-
val holderName = LazyLocalName.fresh(xname)
140-
val initName = LazyLocalInitName.fresh(xname)
141-
val tpe = x.tpe.widen.resultType.widen
142-
143-
val holderType =
144-
if (tpe isRef defn.IntClass) "LazyInt"
145-
else if (tpe isRef defn.LongClass) "LazyLong"
146-
else if (tpe isRef defn.BooleanClass) "LazyBoolean"
147-
else if (tpe isRef defn.FloatClass) "LazyFloat"
148-
else if (tpe isRef defn.DoubleClass) "LazyDouble"
149-
else if (tpe isRef defn.ByteClass) "LazyByte"
150-
else if (tpe isRef defn.CharClass) "LazyChar"
151-
else if (tpe isRef defn.ShortClass) "LazyShort"
152-
else "LazyRef"
153-
154-
155-
val holderImpl = ctx.requiredClass("dotty.runtime." + holderType)
156-
157-
val holderSymbol = ctx.newSymbol(x.symbol.owner, holderName, containerFlags, holderImpl.typeRef, coord = x.pos)
158-
val initSymbol = ctx.newSymbol(x.symbol.owner, initName, initFlags, MethodType(Nil, tpe), coord = x.pos)
159-
val result = ref(holderSymbol).select(lazyNme.value).withPos(x.pos)
160-
val flag = ref(holderSymbol).select(lazyNme.initialized)
161-
val initer = valueInitter.changeOwnerAfter(x.symbol, initSymbol, this)
162-
val initBody =
163-
adaptToType(
164-
ref(holderSymbol).select(defn.Object_synchronized).appliedTo(
165-
adaptToType(mkNonThreadSafeDef(result, flag, initer, nullables = Nil), defn.ObjectType)),
166-
tpe)
167-
val initTree = DefDef(initSymbol, initBody)
168-
val holderTree = ValDef(holderSymbol, New(holderImpl.typeRef, List()))
169-
val methodBody = tpd.If(flag.ensureApplied,
170-
result.ensureApplied,
171-
ref(initSymbol).ensureApplied).ensureConforms(tpe)
172-
173-
val methodTree = DefDef(x.symbol.asTerm, methodBody)
174-
ctx.debuglog(s"found a lazy val ${x.show},\nrewrote with ${holderTree.show}")
175-
Thicket(holderTree, initTree, methodTree)
149+
val xname = x.name.asTermName
150+
val tpe = x.tpe.widen.resultType.widen
151+
152+
// val x$lzy = new scala.runtime.LazyInt()
153+
val holderName = LazyLocalName.fresh(xname)
154+
val holderImpl = defn.LazyHolder()(ctx)(tpe.typeSymbol)
155+
val holderSymbol = ctx.newSymbol(x.symbol.owner, holderName, containerFlags, holderImpl.typeRef, coord = x.pos)
156+
val holderTree = ValDef(holderSymbol, New(holderImpl.typeRef, Nil))
157+
158+
val holderRef = ref(holderSymbol)
159+
val getValue = holderRef.select(lazyNme.value).ensureApplied.withPos(x.pos)
160+
val initialized = holderRef.select(lazyNme.initialized).ensureApplied
161+
162+
// def x$lzycompute(): Int = x$lzy.synchronized {
163+
// if (x$lzy.initialized()) x$lzy.value()
164+
// else x$lzy.initialize(<RHS>)
165+
// }
166+
val initName = LazyLocalInitName.fresh(xname)
167+
val initSymbol = ctx.newSymbol(x.symbol.owner, initName, initFlags, MethodType(Nil, tpe), coord = x.pos)
168+
val rhs = x.rhs.changeOwnerAfter(x.symbol, initSymbol, this)
169+
val initialize = holderRef.select(lazyNme.initialize).appliedTo(rhs)
170+
val initBody = holderRef
171+
.select(defn.Object_synchronized)
172+
.appliedToType(tpe)
173+
.appliedTo(If(initialized, getValue, initialize).ensureConforms(tpe))
174+
val initTree = DefDef(initSymbol, initBody)
175+
176+
// def x(): Int = if (x$lzy.initialized()) x$lzy.value() else x$lzycompute()
177+
val accessorBody = If(initialized, getValue, ref(initSymbol).ensureApplied).ensureConforms(tpe)
178+
val accessor = DefDef(x.symbol.asTerm, accessorBody)
179+
180+
ctx.debuglog(s"found a lazy val ${x.show},\nrewrote with ${holderTree.show}")
181+
Thicket(holderTree, initTree, accessor)
176182
}
177183

178184

@@ -458,6 +464,7 @@ object LazyVals {
458464
val result: TermName = "result".toTermName
459465
val value: TermName = "value".toTermName
460466
val initialized: TermName = "initialized".toTermName
467+
val initialize: TermName = "initialize".toTermName
461468
val retry: TermName = "retry".toTermName
462469
}
463470
}

compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,4 +500,50 @@ class TestBCode extends DottyBytecodeTest {
500500
liftReceiver)
501501
}
502502
}
503+
504+
/** Test that the size of the lazy val initialiazer is under a certain threshold
505+
*
506+
* - Fix to #5340 reduced the size from 39 instructions to 34
507+
* - Fix to #505 reduced the size from 34 instructions to 32
508+
*/
509+
@Test def i5340 = {
510+
val source =
511+
"""class Test {
512+
| def test = {
513+
| lazy val x = 1
514+
| x
515+
| }
516+
|}
517+
""".stripMargin
518+
519+
checkBCode(source) { dir =>
520+
val clsIn = dir.lookupName("Test.class", directory = false).input
521+
val clsNode = loadClassNode(clsIn)
522+
val method = getMethod(clsNode, "x$lzyINIT1$1")
523+
assertEquals(32, instructionsFromMethod(method).size)
524+
}
525+
}
526+
527+
/** Test that synchronize blocks don't box */
528+
@Test def i505 = {
529+
val source =
530+
"""class Test {
531+
| def test: Int = synchronized(1)
532+
|}
533+
""".stripMargin
534+
535+
checkBCode(source) { dir =>
536+
val clsIn = dir.lookupName("Test.class", directory = false).input
537+
val clsNode = loadClassNode(clsIn)
538+
val method = getMethod(clsNode, "test")
539+
540+
val doBox = instructionsFromMethod(method).exists {
541+
case Invoke(_, _, name, _, _) =>
542+
name == "boxToInteger" || name == "unboxToInt"
543+
case _ =>
544+
false
545+
}
546+
assertFalse(doBox)
547+
}
548+
}
503549
}

library/src/dotty/runtime/LazyHolders.scala

Lines changed: 0 additions & 44 deletions
This file was deleted.

tests/run/i505.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
object Test {
2+
def main(args: Array[String]): Unit = {
3+
val a: Int = synchronized(1)
4+
val b: Long = synchronized(1L)
5+
val c: Boolean = synchronized(true)
6+
val d: Float = synchronized(1f)
7+
val e: Double = synchronized(1.0)
8+
val f: Byte = synchronized(1.toByte)
9+
val g: Char = synchronized('1')
10+
val h: Short = synchronized(1.toShort)
11+
val i: String = synchronized("Hello")
12+
val j: List[Int] = synchronized(List(1))
13+
synchronized(())
14+
}
15+
}

tests/run/i5340.check

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Int
2+
Long
3+
Float
4+
Double
5+
Byte
6+
Char
7+
Short
8+
String

tests/run/i5340.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
object Test {
2+
def main(args: Array[String]): Unit = {
3+
lazy val a: Int = { println("Int"); 1 }
4+
lazy val b: Long = { println("Long"); 1L }
5+
lazy val c: Float = { println("Float"); 1F }
6+
lazy val d: Double = { println("Double"); 1.0 }
7+
lazy val e: Byte = { println("Byte"); 1 }
8+
lazy val f: Char = { println("Char"); '1' }
9+
lazy val g: Short = { println("Short"); 1 }
10+
lazy val h: String = { println("String"); "1" }
11+
lazy val i: Unit = { println("Unit"); () }
12+
13+
a
14+
b
15+
c
16+
d
17+
e
18+
f
19+
g
20+
h
21+
// i // FIXME: See #5350
22+
}
23+
}

0 commit comments

Comments
 (0)