From b9d556574e29820b31f7183ea3e5f17fbe0dc627 Mon Sep 17 00:00:00 2001 From: olsdavis Date: Tue, 17 May 2022 16:57:30 +0200 Subject: [PATCH 1/7] New lazy vals implementation --- .../dotty/tools/dotc/transform/LazyVals.scala | 294 ++++++++++++++++-- .../test-resources/scripting/scriptPath.sc | 2 + .../backend/jvm/DottyBytecodeTests.scala | 2 +- library/src/scala/runtime/LazyVals.scala | 56 ++++ project/MiMaFilters.scala | 16 + tests/init/neg/t3273.check | 3 - tests/run/lazyVals_c3.0.0.check | 4 + tests/run/lazyVals_c3.0.0.scala | 13 + tests/run/lazyVals_c3.1.0.check | 4 + tests/run/lazyVals_c3.1.0.scala | 13 + 10 files changed, 381 insertions(+), 26 deletions(-) create mode 100644 tests/run/lazyVals_c3.0.0.check create mode 100644 tests/run/lazyVals_c3.0.0.scala create mode 100644 tests/run/lazyVals_c3.1.0.check create mode 100644 tests/run/lazyVals_c3.1.0.scala diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index c32ea61cff2b..11e08c1af254 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -23,10 +23,17 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { import LazyVals._ import tpd._ - /** this map contains mutable state of transformation: OffsetDefs to be appended to companion object definitions, - * and number of bits currently used */ - class OffsetInfo(var defs: List[Tree], var ord:Int) + /** + * The map contains the list of the offset trees. + */ + class OffsetInfo(var defs: List[Tree]) + /** + * This map contains mutable state of transformation: OffsetDefs to be appended + * to companion object definitions, and number of bits currently used. + */ + class OldOffsetInfo(defs: List[Tree], var ord: Int) extends OffsetInfo(defs) private val appendOffsetDefs = mutable.Map.empty[Symbol, OffsetInfo] + private val oldAppendOffsetDefs = mutable.Map.empty[Symbol, OldOffsetInfo] override def phaseName: String = LazyVals.name @@ -52,6 +59,20 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { else nullables.toList } + private inline def isOldLazyVals(using ctx: Context): Boolean = + import dotty.tools.dotc.config.ScalaRelease._ + ctx.scalaRelease <= Release3_1 + + private def initBlock(stats: List[Tree])(using Context): Block = stats match + case Nil => throw new IllegalArgumentException("trying to create an empty Block") + case x :: Nil => Block(List(x), EmptyTree) + case x :: xs => Block(stats.init, stats.last) + + private def needsBoxing(tp: Type)(using Context): Boolean = tp != NoType && tp != defn.UnitType && tp.classSymbol.isPrimitiveValueClass + + private def boxIfCan(tp: Type)(using Context): Type = + assert(needsBoxing(tp)) + defn.boxedType(tp) override def prepareForUnit(tree: Tree)(using Context): Context = { if (lazyValNullables == null) @@ -62,7 +83,6 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { override def transformDefDef(tree: DefDef)(using Context): Tree = transformLazyVal(tree) - override def transformValDef(tree: ValDef)(using Context): Tree = transformLazyVal(tree) @@ -101,13 +121,13 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } } - - /** Append offset fields to companion objects - */ + /** + * Append offset fields to companion objects. + */ override def transformTemplate(template: Template)(using Context): Tree = { val cls = ctx.owner.asClass - - appendOffsetDefs.get(cls) match { + + (if isOldLazyVals then oldAppendOffsetDefs else appendOffsetDefs).get(cls) match { case None => template case Some(data) => data.defs.foreach(_.symbol.addAnnotation(Annotation(defn.ScalaStaticAnnot))) @@ -115,16 +135,15 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } } - private def addInFront(prefix: List[Tree], stats: List[Tree]) = stats match { case first :: rest if isSuperConstrCall(first) => first :: prefix ::: rest case _ => prefix ::: stats } - /** Make an eager val that would implement synthetic module. - * Eager val ensures thread safety and has less code generated. - * - */ + /** + * Make an eager val that would implement synthetic module. + * Eager val ensures thread safety and has less code generated. + */ def transformSyntheticModule(tree: ValOrDefDef)(using Context): Thicket = { val sym = tree.symbol val holderSymbol = newSymbol(sym.owner, LazyLocalName.fresh(sym.asTerm.name), @@ -134,7 +153,8 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { Thicket(field, getter) } - /** Desugar a local `lazy val x: Int = ` into: + /** + * Desugar a local `lazy val x: Int = ` into: * * ``` * val x$lzy = new scala.runtime.LazyInt() @@ -186,7 +206,6 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { Thicket(holderTree, initTree, accessor) } - override def transformStats(trees: List[tpd.Tree])(using Context): List[Tree] = { // backend requires field usage to be after field definition // need to bring containers to start of method @@ -274,6 +293,227 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } } + def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { + assert(!(x.symbol is Mutable)) + // generate old code for compatibility + // TODO find more meaningful names than old/new + if isOldLazyVals then + transformMemberDefThreadSafeOld(x) + else + transformMemberDefThreadSafeNew(x) + } + + /** + * Create a threadsafe lazy accessor equivalent to the following code: + * ``` + * private @volatile var _x: AnyRef = null + * @tailrec def x: A = + * _x match + * case current: A => + * current + * case null => + * if CAS(_x, null, Evaluating) then + * var result: AnyRef = null // here, we need `AnyRef` to possibly assign `NULL` + * try + * result = rhs + * nullable = null // if the field is nullable; see `CollectNullableFields` + * if result == null then result = NULL // drop if A is non-nullable + * finally + * if !CAS(_x, Evaluating, result) then + * val lock = _x.asInstanceOf[Waiting] + * CAS(_x, lock, result) + * lock.release() + * x + * case Evaluating => + * CAS(_x, Evaluating, new Waiting) + * x + * case current: Waiting => + * current.awaitRelease() + * x + * case NULL => null + * ``` + * Where `Evaluating` and `NULL` are represented by `object`s and `Waiting` by a class that + * allows awaiting the completion of the evaluation. Note that since tail-recursive + * functions are transformed *before* lazy-vals, this implementation directly implements + * the resulting loop. `PatternMatcher` coming before `LazyVals`, the pattern matching block + * is implemented using if-s. That is: + * + * ``` + * private @volatile var _x: AnyRef = null + * def x: A = + * while true do + * val current: AnyRef = _x + * if current == null then + * if CAS(_x, null, Evaluating) then + * var result: AnyRef = null + * try + * result = rhs + * nullable = null + * if result == null then result = NULL + * finally + * if !CAS(_x, Evaluating, result) then + * val lock = _x.asInstanceOf[Waiting] + * CAS(_x, lock, result) + * lock.release() + * else + * if current.isInstanceOf[Evaluating] then + * CAS(current, Evaluating, new Waiting) + * else if current.isInstanceOf[NULL] then + * null + * else if current.isInstanceOf[Waiting] then + * current.asInstanceOf[Waiting].awaitRelease() + * else + * current.asInstanceOf[A] + * end while + * ``` + * + * @param methodSymbol the symbol of the new method + * @param claz the class containing this lazy val field + * @param target the target synthetic field + * @param rhs the right-hand side expression of the lazy val + * @param tp the type of the lazy val + * @param offset the offset of the field in the bitmap + * @param getFlag a flag for the volatile get function + * @param objCasFlag a flag for the CAS function operating on objects + * @param waiting a reference to the `Waiting` runtime class + * @param evaluating a reference to the `Evaluating` runtime object + * @param nullValue a reference to the `NULL` runtime object + */ + def mkThreadSafeDefNew(methodSymbol: TermSymbol, + claz: ClassSymbol, + target: Symbol, + rhs: Tree, + tp: Type, + offset: Tree, + objCasFlag: Tree, + waiting: Tree, + evaluating: Tree, + nullValue: Tree, + thiz: Tree)(using Context): DefDef = { + val discardSymb = newSymbol(methodSymbol, lazyNme.discard, Method | Synthetic, MethodType(Nil)(_ => Nil, _ => defn.UnitType)) + val discardDef = DefDef(discardSymb, initBlock( + objCasFlag.appliedTo(thiz, offset, evaluating, Select(New(waiting), StdNames.nme.CONSTRUCTOR).ensureApplied) + :: Return(unitLiteral, discardSymb) :: Nil)) + // if observed a null value + val unevaluated = { + // var res: AnyRef + val resSymb = newSymbol(methodSymbol, lazyNme.result, Synthetic | Mutable, defn.ObjectType) + // releasing block in finally + val lockRel = { + val lockSymb = newSymbol(methodSymbol, lazyNme.lock, Synthetic, waiting.typeOpt) + initBlock(ValDef(lockSymb, ref(target).cast(waiting.typeOpt)) + :: objCasFlag.appliedTo(thiz, offset, ref(lockSymb), ref(resSymb)) + :: ref(lockSymb).select(lazyNme.RLazyVals.waitingRelease).ensureApplied :: Nil) + } + // finally block + val fin = If( + objCasFlag.appliedTo(thiz, offset, evaluating, ref(resSymb)).equal(Literal(Constant(false))), + lockRel, + EmptyTree + ).withType(defn.UnitType) + // entire try block + val evaluate = Try( + initBlock( + Assign(ref(resSymb), if needsBoxing(tp) && rhs != EmptyTree then rhs.ensureConforms(boxIfCan(tp)) else rhs) // try result = rhs + :: nullOut(nullableFor(methodSymbol)) + ::: If(ref(resSymb).equal(nullLiteral), Assign(ref(resSymb), nullValue), EmptyTree).withType(defn.UnitType) // if result == null then result = NULL + :: Nil + ), + Nil, + fin + ) + // if CAS(...) + If( + objCasFlag.appliedTo(thiz, offset, nullLiteral, evaluating), + initBlock(ValDef(resSymb, nullLiteral) // var result: AnyRef = null + :: evaluate // try ... finally ... + :: Nil), + EmptyTree + ).withType(defn.UnitType) + } + val current = newSymbol(methodSymbol, lazyNme.current, Synthetic, defn.ObjectType) + val ifNotNull = + If( + ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(evaluating), + ref(discardSymb).ensureApplied, + // not an Evaluating + If( + ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(nullValue), + Return(defaultValue(tp), methodSymbol), + // not a NULL + If( + ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(waiting), + ref(current).select(defn.Any_asInstanceOf).appliedToTypeTree(waiting).select(lazyNme.RLazyVals.waitingAwaitRelease).ensureApplied, + // not a Waiting, then is an A + Return(ref(current).ensureConforms(tp), methodSymbol) + ) + ) + ) + val body = initBlock(ValDef(current, ref(target)) :: If(ref(current).equal(nullLiteral), unevaluated, ifNotNull) :: Nil) + val mainLoop = WhileDo(EmptyTree, body) // becomes: while (true) do { body } + val ret = DefDef(methodSymbol, initBlock(discardDef :: mainLoop :: Nil)) + ret + } + + def transformMemberDefThreadSafeNew(x: ValOrDefDef)(using Context): Thicket = { + import dotty.tools.dotc.core.Types._ + import dotty.tools.dotc.core.Flags._ + + val runtimeModule = "scala.runtime.LazyVals" + val tpe = x.tpe.widen.resultType.widen + val claz = x.symbol.owner.asClass + val thizClass = Literal(Constant(claz.info)) + val helperModule = requiredModule(runtimeModule) + var offsetSymbol: TermSymbol | Null = null + + def offsetName(id: Int) = s"${StdNames.nme.LAZY_FIELD_OFFSET}${if (x.symbol.owner.is(Module)) "_m_" else ""}$id".toTermName + + val containerName = LazyLocalName.fresh(x.name.asTermName) + val containerSymbol = newSymbol(claz, containerName, containerFlags, defn.ObjectType).enteredAfter(this) + containerSymbol.addAnnotation(Annotation(defn.VolatileAnnot)) // private @volatile var _x: AnyRef + val stat = x.symbol.isStatic + if stat then + containerSymbol.setFlag(JavaStatic) + val getOffset = + if stat then + Select(ref(helperModule), lazyNme.RLazyVals.getStaticOffset) + else + Select(ref(helperModule), lazyNme.RLazyVals.getOffset) + val containerTree = ValDef(containerSymbol, nullLiteral) + def staticOrFieldOff: Tree = getOffset.appliedTo(thizClass, Literal(Constant(containerName.toString))) + + // create an offset for this lazy val + appendOffsetDefs.get(claz) match + case Some(info) => + offsetSymbol = newSymbol(claz, offsetName(info.defs.size), Synthetic, defn.LongType).enteredAfter(this) + offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot)) + val offsetTree = ValDef(offsetSymbol.nn, staticOrFieldOff) + info.defs = offsetTree :: info.defs + case None => + offsetSymbol = newSymbol(claz, offsetName(0), Synthetic, defn.LongType).enteredAfter(this) + offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot)) + val offsetTree = ValDef(offsetSymbol.nn, staticOrFieldOff) + appendOffsetDefs += (claz -> new OffsetInfo(List(offsetTree))) + + val waiting = requiredClass(s"$runtimeModule.${lazyNme.RLazyVals.waiting}") + val evaluating = Select(ref(helperModule), lazyNme.RLazyVals.evaluating) + val nullValue = Select(ref(helperModule), lazyNme.RLazyVals.nullValue) + val objCas = Select(ref(helperModule), lazyNme.RLazyVals.objCas) + + val offset = ref(offsetSymbol.nn) + + val swapOver = + if stat then + tpd.clsOf(x.symbol.owner.typeRef) + else + This(claz) + + val methodSymbol = x.symbol.asTerm + val accessor = mkThreadSafeDefNew(methodSymbol, claz, containerSymbol, x.rhs, tpe, offset, objCas, + ref(waiting), evaluating, nullValue, swapOver) + Thicket(containerTree, accessor) + } + /** Create a threadsafe lazy accessor equivalent to such code * ``` * def methodSymbol(): Int = { @@ -305,7 +545,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { * } * ``` */ - def mkThreadSafeDef(methodSymbol: TermSymbol, + def mkThreadSafeDefOld(methodSymbol: TermSymbol, claz: ClassSymbol, ord: Int, target: Symbol, @@ -374,9 +614,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { DefDef(methodSymbol, loop) } - def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { - assert(!(x.symbol is Mutable)) - + def transformMemberDefThreadSafeOld(x: ValOrDefDef)(using Context): Thicket = { val tpe = x.tpe.widen.resultType.widen val claz = x.symbol.owner.asClass val thizClass = Literal(Constant(claz.info)) @@ -390,7 +628,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def offsetName(id: Int) = s"${StdNames.nme.LAZY_FIELD_OFFSET}${if (x.symbol.owner.is(Module)) "_m_" else ""}$id".toTermName // compute or create appropriate offsetSymbol, bitmap and bits used by current ValDef - appendOffsetDefs.get(claz) match { + oldAppendOffsetDefs.get(claz) match { case Some(info) => val flagsPerLong = (64 / scala.runtime.LazyVals.BITS_PER_LAZY_VAL).toInt info.ord += 1 @@ -435,7 +673,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { val state = Select(ref(helperModule), lazyNme.RLazyVals.state) val cas = Select(ref(helperModule), lazyNme.RLazyVals.cas) - val accessor = mkThreadSafeDef(x.symbol.asTerm, claz, ord, containerSymbol, x.rhs, tpe, offset, getFlag, state, cas, setFlag, wait) + val accessor = mkThreadSafeDefOld(x.symbol.asTerm, claz, ord, containerSymbol, x.rhs, tpe, offset, getFlag, state, cas, setFlag, wait) if (flag eq EmptyTree) Thicket(containerTree, accessor) else Thicket(containerTree, flag, accessor) @@ -450,6 +688,14 @@ object LazyVals { import Names.TermName object RLazyVals { import scala.runtime.LazyVals.{Names => N} + val waiting: TermName = N.waiting.toTermName + val waitingAwaitRelease: TermName = N.waitingAwaitRelease.toTermName + val waitingRelease: TermName = N.waitingRelease.toTermName + val evaluating: TermName = N.evaluating.toTermName + val nullValue: TermName = N.nullValue.toTermName + val objCas: TermName = N.objCas.toTermName + val getOffset: TermName = N.getOffset.toTermName + val getStaticOffset: TermName = N.getStaticOffset.toTermName val get: TermName = N.get.toTermName val setFlag: TermName = N.setFlag.toTermName val wait4Notification: TermName = N.wait4Notification.toTermName @@ -458,6 +704,7 @@ object LazyVals { val getOffset: TermName = N.getOffset.toTermName val getOffsetStatic: TermName = "getOffsetStatic".toTermName val getDeclaredField: TermName = "getDeclaredField".toTermName + } val flag: TermName = "flag".toTermName val state: TermName = "state".toTermName @@ -466,5 +713,8 @@ object LazyVals { val initialized: TermName = "initialized".toTermName val initialize: TermName = "initialize".toTermName val retry: TermName = "retry".toTermName + val current: TermName = "current".toTermName + val lock: TermName = "lock".toTermName + val discard: TermName = "discard".toTermName } } diff --git a/compiler/test-resources/scripting/scriptPath.sc b/compiler/test-resources/scripting/scriptPath.sc index 46cd5e8a7385..837bd674aad8 100755 --- a/compiler/test-resources/scripting/scriptPath.sc +++ b/compiler/test-resources/scripting/scriptPath.sc @@ -16,6 +16,8 @@ val pathEntries = System.getenv("PATH").split(psep).toList System.err.printf("sun.java.command: %s\n", sys.props("sun.java.command")) System.err.printf("first 5 PATH entries:\n%s\n",pathEntries.take(5).mkString("\n")) + printf("script.path: %s\n",path.norm) + assert(path.endsWith("scriptPath.sc"),s"actual path [$path]") } extension(s: String) diff --git a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala index 2c618ea91e96..3056f870d0fc 100644 --- a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala @@ -597,7 +597,7 @@ class DottyBytecodeTests extends DottyBytecodeTest { val clsIn = dir.lookupName("Test.class", directory = false).input val clsNode = loadClassNode(clsIn) val method = getMethod(clsNode, "test") - assertEquals(88, instructionsFromMethod(method).size) + assertEquals(122, instructionsFromMethod(method).size) } } diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index 7a66639a826a..7cc80b95c297 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -25,6 +25,7 @@ object LazyVals { val processors = java.lang.Runtime.getRuntime.nn.availableProcessors() 8 * processors * processors } + private[this] val monitors: Array[Object] = Array.tabulate(base)(_ => new Object) @@ -40,6 +41,41 @@ object LazyVals { /* ------------- Start of public API ------------- */ + /** + * Used to indicate the state of a lazy val that is being + * evaluated and of which other threads await the result. + */ + final class Waiting: + private var done = false + + /** + * Wakes up waiting threads. Called on completion of the evaluation + * of lazy val's right-hand side. + */ + def release(): Unit = synchronized { + done = true + notifyAll() + } + + /** + * Awaits the completion of the evaluation of lazy val's right-hand side. + */ + def awaitRelease(): Unit = synchronized { + while !done do wait() + } + + /** + * Used to indicate the state of a lazy val that is currently being + * evaluated with no other thread awaiting its result. + */ + object Evaluating + + /** + * Used to indicate the state of a lazy val that has been evaluated to + * `null`. + */ + object NULL + final val BITS_PER_LAZY_VAL = 2L def STATE(cur: Long, ord: Int): Long = { @@ -57,6 +93,12 @@ object LazyVals { unsafe.compareAndSwapLong(t, offset, e, n) } + def objCAS(t: Object, offset: Long, exp: Object, n: Object): Boolean = { + if (debug) + println(s"objCAS($t, $exp, $n)") + unsafe.compareAndSwapObject(t, offset, exp, n) + } + def setFlag(t: Object, offset: Long, v: Int, ord: Int): Unit = { if (debug) println(s"setFlag($t, $offset, $v, $ord)") @@ -110,6 +152,13 @@ object LazyVals { r } + def getStaticOffset(clz: Class[_], name: String): Long = { + val r = unsafe.staticFieldOffset(clz.getDeclaredField(name)) + if (debug) + println(s"getStaticOffset($clz, $name) = $r") + r + } + def getOffsetStatic(field: java.lang.reflect.Field) = @nowarn val r = unsafe.objectFieldOffset(field) @@ -119,11 +168,18 @@ object LazyVals { object Names { + final val waiting = "Waiting" + final val evaluating = "Evaluating" + final val nullValue = "NULL" + final val waitingAwaitRelease = "awaitRelease" + final val waitingRelease = "release" final val state = "STATE" final val cas = "CAS" + final val objCas = "objCAS" final val setFlag = "setFlag" final val wait4Notification = "wait4Notification" final val get = "get" final val getOffset = "getOffset" + final val getStaticOffset = "getStaticOffset" } } diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 12ae3487cade..927b54d86793 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -4,6 +4,22 @@ import com.typesafe.tools.mima.core._ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.MappedAlternative"), + + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getStaticOffset"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.objCAS"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.evaluating"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.getStaticOffset"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.nullValue"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.objCas"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waiting"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waitingAwaitRelease"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waitingRelease"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$Evaluating$"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$NULL$"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$Waiting"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.LazyVals.Evaluating"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.LazyVals.NULL"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.pureFunctions"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.captureChecking"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$pureFunctions$"), diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index e548a5964cac..6dd29cbe8adb 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -12,9 +12,6 @@ | Access non-initialized value num1. Promotion trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] | ^^^^ --- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ -5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Cannot prove the method argument is hot. Only hot values are safe to leak. | Found = Fun { this = ThisRef[object Test], owner = object Test }. Calling trace: | -> object Test { [ t3273.scala:3 ] diff --git a/tests/run/lazyVals_c3.0.0.check b/tests/run/lazyVals_c3.0.0.check new file mode 100644 index 000000000000..a0aad90603ed --- /dev/null +++ b/tests/run/lazyVals_c3.0.0.check @@ -0,0 +1,4 @@ +computing x +x +computing y +y \ No newline at end of file diff --git a/tests/run/lazyVals_c3.0.0.scala b/tests/run/lazyVals_c3.0.0.scala new file mode 100644 index 000000000000..5df4069bc94c --- /dev/null +++ b/tests/run/lazyVals_c3.0.0.scala @@ -0,0 +1,13 @@ +// Compiled with 3.0.0 and run with current compiler +class Foo: + lazy val x = + println("computing x") + "x" + lazy val y = + println("computing y") + "y" + +@main def Test = + val foo = new Foo + println(foo.x) + println(foo.y) diff --git a/tests/run/lazyVals_c3.1.0.check b/tests/run/lazyVals_c3.1.0.check new file mode 100644 index 000000000000..a0aad90603ed --- /dev/null +++ b/tests/run/lazyVals_c3.1.0.check @@ -0,0 +1,4 @@ +computing x +x +computing y +y \ No newline at end of file diff --git a/tests/run/lazyVals_c3.1.0.scala b/tests/run/lazyVals_c3.1.0.scala new file mode 100644 index 000000000000..45e796ab46d3 --- /dev/null +++ b/tests/run/lazyVals_c3.1.0.scala @@ -0,0 +1,13 @@ +// Compiled with 3.1.0 and run with current compiler +class Foo: + lazy val x = + println("computing x") + "x" + lazy val y = + println("computing y") + "y" + +@main def Test = + val foo = new Foo + println(foo.x) + println(foo.y) \ No newline at end of file From 3ef3a199d8a9d422a51150a23f5ed31fbc01024e Mon Sep 17 00:00:00 2001 From: Szymon Rodziewicz Date: Wed, 18 May 2022 16:49:09 +0200 Subject: [PATCH 2/7] New version of Lazy Vals implementation --- .../lazyvals/ContendedInitialization.scala | 49 +++ .../lazyvals/InitializedAccess.scala | 29 ++ .../tools/benchmarks/lazyvals/LazyVals.scala | 7 + .../lazyvals/UninitializedAccess copy.scala | 29 ++ .../scala-parallel-collections | 2 +- .../dotty/tools/dotc/ast/TreeTypeMap.scala | 1 + .../tools/dotc/config/ScalaSettings.scala | 1 + .../dotty/tools/dotc/transform/LazyVals.scala | 341 ++++++++++-------- .../tools/dotc/transform/TreeChecker.scala | 1 - .../test-resources/scripting/scriptPath.sc | 2 - .../backend/jvm/DottyBytecodeTests.scala | 2 +- .../tools/dotc/printing/PrintingTest.scala | 16 +- library/src/scala/runtime/LazyVals.scala | 44 +-- tests/init/neg/t3273.check | 3 + .../transformed/lazy-vals-legacy.check | 59 +++ .../transformed/lazy-vals-legacy.flags | 1 + .../transformed/lazy-vals-legacy.scala | 3 + .../printing/transformed/lazy-vals-new.check | 83 +++++ .../printing/transformed/lazy-vals-new.scala | 3 + tests/run/lazyVals-legacy.check | 2 + tests/run/lazyVals-legacy.flags | 1 + tests/run/lazyVals-legacy.scala | 12 + tests/run/serialization-new.check | 7 + tests/run/serialization-new.scala | 16 +- 24 files changed, 518 insertions(+), 196 deletions(-) create mode 100644 bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala create mode 100644 bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala create mode 100644 bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala create mode 100644 bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala create mode 100644 tests/printing/transformed/lazy-vals-legacy.check create mode 100644 tests/printing/transformed/lazy-vals-legacy.flags create mode 100644 tests/printing/transformed/lazy-vals-legacy.scala create mode 100644 tests/printing/transformed/lazy-vals-new.check create mode 100644 tests/printing/transformed/lazy-vals-new.scala create mode 100644 tests/run/lazyVals-legacy.check create mode 100644 tests/run/lazyVals-legacy.flags create mode 100644 tests/run/lazyVals-legacy.scala diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala new file mode 100644 index 000000000000..fb2cedbb7d41 --- /dev/null +++ b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala @@ -0,0 +1,49 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit +import java.util.concurrent.{Executors, ExecutorService} + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +class ContendedInitialization { + + @Param(Array("2000000", "5000000")) + var size: Int = _ + + @Param(Array("2", "4", "8")) + var nThreads: Int = _ + + var executor: ExecutorService = _ + + @Setup + def prepare: Unit = { + executor = Executors.newFixedThreadPool(nThreads) + } + + @TearDown + def cleanup: Unit = { + executor.shutdown() + executor = null + } + + @Benchmark + def measureContended(bh: Blackhole): Unit = { + val array = Array.fill(size)(new LazyHolder) + val task: Runnable = () => + for (elem <- array) bh.consume(elem.value) + + val futures = + for (_ <- 0 until nThreads) yield + executor.submit(task) + + futures.foreach(_.get()) + } +} diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala new file mode 100644 index 000000000000..a00419d5356b --- /dev/null +++ b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala @@ -0,0 +1,29 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class InitializedAccess { + + var holder: LazyHolder = _ + + @Setup + def prepare: Unit = { + holder = new LazyHolder + holder.value + } + + @Benchmark + def measureInitialized(bh: Blackhole) = { + bh.consume(holder.value) + } +} \ No newline at end of file diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala new file mode 100644 index 000000000000..fffddca41648 --- /dev/null +++ b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala @@ -0,0 +1,7 @@ +package dotty.tools.benchmarks.lazyvals + +object LazyVals { + class LazyHolder { + lazy val value = 1 + } +} \ No newline at end of file diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala new file mode 100644 index 000000000000..c7d6eafccf9f --- /dev/null +++ b/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala @@ -0,0 +1,29 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class UninitializedAccess { + + var holder: LazyHolder = _ + + @Setup + def prepare: Unit = { + holder = new LazyHolder + } + + @Benchmark + def measureInitialized(bh: Blackhole) = { + bh.consume(holder) + bh.consume(holder.value) + } +} \ No newline at end of file diff --git a/community-build/community-projects/scala-parallel-collections b/community-build/community-projects/scala-parallel-collections index a6bd648bb188..1cd213661a68 160000 --- a/community-build/community-projects/scala-parallel-collections +++ b/community-build/community-projects/scala-parallel-collections @@ -1 +1 @@ -Subproject commit a6bd648bb188a65ab36be07e956e52fe25f64d67 +Subproject commit 1cd213661a682e7c9947a1d1777526f01225da56 diff --git a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala index 71998aff9304..51d3aba8a2b1 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala @@ -7,6 +7,7 @@ import Types._, Contexts._, Flags._ import Symbols._, Annotations._, Trees._, Symbols._, Constants.Constant import Decorators._ import dotty.tools.dotc.transform.SymUtils._ +import transform.LazyVals /** A map that applies three functions and a substitution together to a tree and * makes sure they are coordinated so that the result is well-typed. The functions are diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index a2dba94ad9fc..52461f5f9c80 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -241,6 +241,7 @@ private sealed trait XSettings: val XcheckMacros: Setting[Boolean] = BooleanSetting("-Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros")) val XmainClass: Setting[String] = StringSetting("-Xmain-class", "path", "Class for manifest's Main-Class entry (only useful with -d )", "") val XimplicitSearchLimit: Setting[Int] = IntSetting("-Ximplicit-search-limit", "Maximal number of expressions to be generated in an implicit search", 50000) + val XlegacyLazyVals: Setting[Boolean] = BooleanSetting("-Xlegacy-lazy-values", "Use legacy lazy vals implementations") val XmixinForceForwarders = ChoiceSetting( name = "-Xmixin-force-forwarders", diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index 11e08c1af254..9ce093174447 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -31,9 +31,9 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { * This map contains mutable state of transformation: OffsetDefs to be appended * to companion object definitions, and number of bits currently used. */ - class OldOffsetInfo(defs: List[Tree], var ord: Int) extends OffsetInfo(defs) + class LegacyOffsetInfo(defs: List[Tree], var ord: Int) extends OffsetInfo(defs) private val appendOffsetDefs = mutable.Map.empty[Symbol, OffsetInfo] - private val oldAppendOffsetDefs = mutable.Map.empty[Symbol, OldOffsetInfo] + private val oldAppendOffsetDefs = mutable.Map.empty[Symbol, LegacyOffsetInfo] override def phaseName: String = LazyVals.name @@ -59,14 +59,8 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { else nullables.toList } - private inline def isOldLazyVals(using ctx: Context): Boolean = - import dotty.tools.dotc.config.ScalaRelease._ - ctx.scalaRelease <= Release3_1 - - private def initBlock(stats: List[Tree])(using Context): Block = stats match - case Nil => throw new IllegalArgumentException("trying to create an empty Block") - case x :: Nil => Block(List(x), EmptyTree) - case x :: xs => Block(stats.init, stats.last) + private inline def isLegacyLazyVals(using ctx: Context): Boolean = + ctx.settings.XlegacyLazyVals.value private def needsBoxing(tp: Type)(using Context): Boolean = tp != NoType && tp != defn.UnitType && tp.classSymbol.isPrimitiveValueClass @@ -126,8 +120,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { */ override def transformTemplate(template: Template)(using Context): Tree = { val cls = ctx.owner.asClass - - (if isOldLazyVals then oldAppendOffsetDefs else appendOffsetDefs).get(cls) match { + (if isLegacyLazyVals then oldAppendOffsetDefs else appendOffsetDefs).get(cls) match { case None => template case Some(data) => data.defs.foreach(_.symbol.addAnnotation(Annotation(defn.ScalaStaticAnnot))) @@ -293,212 +286,248 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } } - def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { - assert(!(x.symbol is Mutable)) - // generate old code for compatibility - // TODO find more meaningful names than old/new - if isOldLazyVals then - transformMemberDefThreadSafeOld(x) - else - transformMemberDefThreadSafeNew(x) - } - /** * Create a threadsafe lazy accessor equivalent to the following code: * ``` * private @volatile var _x: AnyRef = null * @tailrec def x: A = - * _x match - * case current: A => - * current - * case null => - * if CAS(_x, null, Evaluating) then - * var result: AnyRef = null // here, we need `AnyRef` to possibly assign `NULL` - * try - * result = rhs - * nullable = null // if the field is nullable; see `CollectNullableFields` - * if result == null then result = NULL // drop if A is non-nullable - * finally - * if !CAS(_x, Evaluating, result) then - * val lock = _x.asInstanceOf[Waiting] - * CAS(_x, lock, result) - * lock.release() - * x - * case Evaluating => - * CAS(_x, Evaluating, new Waiting) - * x - * case current: Waiting => - * current.awaitRelease() - * x - * case NULL => null + * _x match + * case current: A => + * current + * case NullValue => null + * case null => + * if CAS(_x, null, Evaluating) then + * var result: AnyRef = null // here, we need `AnyRef` to possibly assign `NullValue` + * try + * result = rhs + * nullable = null // if the field is nullable; see `CollectNullableFields` + * finally + * if result == null then result = NullValue // drop if A is non-nullable + * if !CAS(_x, Evaluating, result) then + * val lock = _x.asInstanceOf[Waiting] + * CAS(_x, lock, result) + * lock.release() + * x + * case Evaluating => + * CAS(_x, Evaluating, new Waiting) + * x + * case current: Waiting => + * current.awaitRelease() + * x * ``` - * Where `Evaluating` and `NULL` are represented by `object`s and `Waiting` by a class that + * Where `Evaluating` and `NullValue` are represented by `object`s and `Waiting` by a class that * allows awaiting the completion of the evaluation. Note that since tail-recursive * functions are transformed *before* lazy-vals, this implementation directly implements * the resulting loop. `PatternMatcher` coming before `LazyVals`, the pattern matching block - * is implemented using if-s. That is: + * is implemented using if-s. Additionally, the code can be optimized to be better inlined by + * introducing separate function for computing the lazy value and awaiting it. That is: * * ``` - * private @volatile var _x: AnyRef = null + * private var _x: AnyRef = null + * * def x: A = - * while true do + * if !(_x == null || _x.isInstanceOf[LazyValControlState]) then + * return _x.asInstanceOf[A] + * else if _x == NullValue then + * return null + * else + * return x_compute() + * + * private def x_compute() = + * while do * val current: AnyRef = _x * if current == null then * if CAS(_x, null, Evaluating) then * var result: AnyRef = null * try - * result = rhs * nullable = null - * if result == null then result = NULL + * if result == null then result = NullValue + * return result * finally - * if !CAS(_x, Evaluating, result) then - * val lock = _x.asInstanceOf[Waiting] - * CAS(_x, lock, result) - * lock.release() + * if !CAS(_x, Evaluating, result) then + * val lock = _x.asInstanceOf[Waiting] + * CAS(_x, lock, result) + * lock.release() * else - * if current.isInstanceOf[Evaluating] then - * CAS(current, Evaluating, new Waiting) - * else if current.isInstanceOf[NULL] then - * null - * else if current.isInstanceOf[Waiting] then - * current.asInstanceOf[Waiting].awaitRelease() + * if current.isInstanceOf[LazyValControlState] then + * if current.isInstanceOf[Evaluating] then // To avoid creating Waiting instance + * CAS(current, Evaluating, new Waiting) + * else if current.isInstanceOf[NullValue] then + * return null + * else if current.isInstanceOf[Waiting] then + * current.asInstanceOf[Waiting].await() * else - * current.asInstanceOf[A] + * return current.asInstanceOf[A] * end while - * ``` + * * ``` * - * @param methodSymbol the symbol of the new method + * @param memberDef the transformed lazy field member definition * @param claz the class containing this lazy val field * @param target the target synthetic field - * @param rhs the right-hand side expression of the lazy val - * @param tp the type of the lazy val - * @param offset the offset of the field in the bitmap - * @param getFlag a flag for the volatile get function - * @param objCasFlag a flag for the CAS function operating on objects - * @param waiting a reference to the `Waiting` runtime class - * @param evaluating a reference to the `Evaluating` runtime object - * @param nullValue a reference to the `NULL` runtime object + * @param offset the offset of the field in the storage allocation of the class + * @param thiz a reference to the transformed class + * @param helperModule the symbol of the runtime LazyVals helper module + * @param runtimeModule the runtime LazyVals module path + */ - def mkThreadSafeDefNew(methodSymbol: TermSymbol, + def mkThreadSafeDef(memberDef: ValOrDefDef, claz: ClassSymbol, target: Symbol, - rhs: Tree, - tp: Type, offset: Tree, - objCasFlag: Tree, - waiting: Tree, - evaluating: Tree, - nullValue: Tree, - thiz: Tree)(using Context): DefDef = { - val discardSymb = newSymbol(methodSymbol, lazyNme.discard, Method | Synthetic, MethodType(Nil)(_ => Nil, _ => defn.UnitType)) - val discardDef = DefDef(discardSymb, initBlock( - objCasFlag.appliedTo(thiz, offset, evaluating, Select(New(waiting), StdNames.nme.CONSTRUCTOR).ensureApplied) - :: Return(unitLiteral, discardSymb) :: Nil)) - // if observed a null value - val unevaluated = { - // var res: AnyRef - val resSymb = newSymbol(methodSymbol, lazyNme.result, Synthetic | Mutable, defn.ObjectType) + thiz: Tree, + helperModule: TermSymbol, + runtimeModule: String)(using Context): (DefDef, DefDef) = { + val tp = memberDef.tpe.widen.resultType.widen + val waiting = ref(requiredClass(s"$runtimeModule.${lazyNme.RLazyVals.waiting}")) + val controlState = ref(requiredClass(s"$runtimeModule.${lazyNme.RLazyVals.controlState}")) + val evaluating = Select(ref(helperModule), lazyNme.RLazyVals.evaluating) + val nullValue = Select(ref(helperModule), lazyNme.RLazyVals.nullValue) + val objCasFlag = Select(ref(helperModule), lazyNme.RLazyVals.objCas) + val accessorMethodSymbol = memberDef.symbol.asTerm + val lazyInitMethodName = LazyLocalInitName.fresh(memberDef.name.asTermName) + val lazyInitMethodSymbol = newSymbol(claz, lazyInitMethodName, Synthetic | Method | Private, MethodType(Nil)(_ => Nil, _ => defn.ObjectType)) + + val rhs = memberDef.rhs + val rhsMappedOwner = rhs.changeOwnerAfter(memberDef.symbol, lazyInitMethodSymbol, this) + val valueSymbol = newSymbol(accessorMethodSymbol, lazyNme.result, Synthetic | Mutable, defn.ObjectType) + val accessorBody = + Block( + ValDef(valueSymbol, ref(target)) :: Nil, + If( // if _x != null && !_x.isInstanceOf[LazyValControlState] then + ref(valueSymbol).select(defn.Any_!=).appliedTo(nullLiteral).select(nme.And).appliedTo(ref(valueSymbol).select(defn.Any_isInstanceOf).appliedToTypeTree(controlState) + .select(nme.UNARY_!).appliedToNone), + Return(ref(valueSymbol).ensureConforms(tp), accessorMethodSymbol), // then return _x.asInstanceOf[A] + If( + ref(valueSymbol).select(defn.Any_==).appliedTo(nullValue), + Return(nullLiteral.ensureConforms(tp), accessorMethodSymbol), + Return(ref(lazyInitMethodSymbol).ensureApplied.ensureConforms(tp), accessorMethodSymbol) // else return x_compute() + ) + ) + ) + + val accessorDef = DefDef(accessorMethodSymbol, accessorBody) + + // if observed a null (uninitialized) value + val initialize = { + // var result: AnyRef + val resSymb = newSymbol(lazyInitMethodSymbol, lazyNme.result, Synthetic | Mutable, defn.ObjectType) // releasing block in finally val lockRel = { - val lockSymb = newSymbol(methodSymbol, lazyNme.lock, Synthetic, waiting.typeOpt) - initBlock(ValDef(lockSymb, ref(target).cast(waiting.typeOpt)) - :: objCasFlag.appliedTo(thiz, offset, ref(lockSymb), ref(resSymb)) - :: ref(lockSymb).select(lazyNme.RLazyVals.waitingRelease).ensureApplied :: Nil) + val lockSymb = newSymbol(lazyInitMethodSymbol, lazyNme.lock, Synthetic, waiting.typeOpt) + Block(ValDef(lockSymb, ref(target).cast(waiting.typeOpt)) + :: objCasFlag.appliedTo(thiz, offset, ref(lockSymb), ref(resSymb)) :: Nil, + ref(lockSymb).select(lazyNme.RLazyVals.waitingRelease).ensureApplied) } // finally block val fin = If( - objCasFlag.appliedTo(thiz, offset, evaluating, ref(resSymb)).equal(Literal(Constant(false))), - lockRel, - EmptyTree - ).withType(defn.UnitType) + objCasFlag.appliedTo(thiz, offset, evaluating, ref(resSymb)).select(nme.UNARY_!).appliedToNone, + lockRel, + unitLiteral + ).withType(defn.UnitType) // entire try block val evaluate = Try( - initBlock( - Assign(ref(resSymb), if needsBoxing(tp) && rhs != EmptyTree then rhs.ensureConforms(boxIfCan(tp)) else rhs) // try result = rhs - :: nullOut(nullableFor(methodSymbol)) - ::: If(ref(resSymb).equal(nullLiteral), Assign(ref(resSymb), nullValue), EmptyTree).withType(defn.UnitType) // if result == null then result = NULL - :: Nil - ), + + Block( + (Assign(ref(resSymb), if needsBoxing(tp) && rhsMappedOwner != EmptyTree then rhsMappedOwner.ensureConforms(boxIfCan(tp)) else rhsMappedOwner) // try result = rhs + :: If( + ref(resSymb).select(defn.Any_==).appliedTo(nullLiteral), + Assign(ref(resSymb), nullValue), + unitLiteral + ) :: Nil) + ::: nullOut(nullableFor(accessorMethodSymbol)), + unitLiteral), Nil, fin ) - // if CAS(...) + // if CAS(_, null, Evaluating) If( objCasFlag.appliedTo(thiz, offset, nullLiteral, evaluating), - initBlock(ValDef(resSymb, nullLiteral) // var result: AnyRef = null - :: evaluate // try ... finally ... - :: Nil), - EmptyTree + Block(ValDef(resSymb, nullLiteral) :: Nil, // var result: AnyRef = null + evaluate), // try ... finally ... + unitLiteral ).withType(defn.UnitType) } - val current = newSymbol(methodSymbol, lazyNme.current, Synthetic, defn.ObjectType) - val ifNotNull = + + val current = newSymbol(lazyInitMethodSymbol, lazyNme.current, Synthetic, defn.ObjectType) + val ifNotUninitialized = If( - ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(evaluating), - ref(discardSymb).ensureApplied, - // not an Evaluating - If( - ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(nullValue), - Return(defaultValue(tp), methodSymbol), - // not a NULL + ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(controlState), + // if a control state If( - ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(waiting), - ref(current).select(defn.Any_asInstanceOf).appliedToTypeTree(waiting).select(lazyNme.RLazyVals.waitingAwaitRelease).ensureApplied, - // not a Waiting, then is an A - Return(ref(current).ensureConforms(tp), methodSymbol) - ) - ) + ref(current).select(defn.Any_==).appliedTo(evaluating), + // if is Evaluating then CAS(_, Evaluating, new Waiting) + Block( + objCasFlag.appliedTo(thiz, offset, evaluating, Select(New(waiting), StdNames.nme.CONSTRUCTOR).ensureApplied) :: Nil, + unitLiteral + ), + // if not Evaluating + If( + ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(waiting), + // if is waiting + ref(current).select(defn.Any_asInstanceOf).appliedToTypeTree(waiting).select(lazyNme.RLazyVals.waitingAwaitRelease, _.info.paramInfoss.exists(_.size == 0)).ensureApplied, + If(ref(current).select(defn.Any_==).appliedTo(nullValue), + Return(nullLiteral, lazyInitMethodSymbol), + unitLiteral + ) + ) + ), + // if not a control state + Return(ref(current), lazyInitMethodSymbol) ) - val body = initBlock(ValDef(current, ref(target)) :: If(ref(current).equal(nullLiteral), unevaluated, ifNotNull) :: Nil) - val mainLoop = WhileDo(EmptyTree, body) // becomes: while (true) do { body } - val ret = DefDef(methodSymbol, initBlock(discardDef :: mainLoop :: Nil)) - ret + + val initBody = Block(ValDef(current, ref(target)) :: Nil, If(ref(current).equal(nullLiteral), initialize, ifNotUninitialized).withType(defn.UnitType)) + val initMainLoop = WhileDo(EmptyTree, initBody) // becomes: while (true) do { body } + val initMethodDef = DefDef(lazyInitMethodSymbol, initMainLoop) + (accessorDef, initMethodDef) + } + + def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { + assert(!(x.symbol is Mutable)) + if isLegacyLazyVals then + transformMemberDefThreadSafeLegacy(x) + else + transformMemberDefThreadSafeNew(x) } def transformMemberDefThreadSafeNew(x: ValOrDefDef)(using Context): Thicket = { import dotty.tools.dotc.core.Types._ import dotty.tools.dotc.core.Flags._ - + val runtimeModule = "scala.runtime.LazyVals" - val tpe = x.tpe.widen.resultType.widen val claz = x.symbol.owner.asClass val thizClass = Literal(Constant(claz.info)) val helperModule = requiredModule(runtimeModule) var offsetSymbol: TermSymbol | Null = null def offsetName(id: Int) = s"${StdNames.nme.LAZY_FIELD_OFFSET}${if (x.symbol.owner.is(Module)) "_m_" else ""}$id".toTermName - val containerName = LazyLocalName.fresh(x.name.asTermName) val containerSymbol = newSymbol(claz, containerName, containerFlags, defn.ObjectType).enteredAfter(this) containerSymbol.addAnnotation(Annotation(defn.VolatileAnnot)) // private @volatile var _x: AnyRef + containerSymbol.addAnnotations(x.symbol.annotations) // pass annotations from original definition val stat = x.symbol.isStatic if stat then containerSymbol.setFlag(JavaStatic) val getOffset = if stat then - Select(ref(helperModule), lazyNme.RLazyVals.getStaticOffset) + Select(ref(helperModule), lazyNme.RLazyVals.getStaticFieldOffset) else - Select(ref(helperModule), lazyNme.RLazyVals.getOffset) + Select(ref(helperModule), lazyNme.RLazyVals.getOffsetStatic) val containerTree = ValDef(containerSymbol, nullLiteral) - def staticOrFieldOff: Tree = getOffset.appliedTo(thizClass, Literal(Constant(containerName.toString))) // create an offset for this lazy val appendOffsetDefs.get(claz) match case Some(info) => offsetSymbol = newSymbol(claz, offsetName(info.defs.size), Synthetic, defn.LongType).enteredAfter(this) offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot)) - val offsetTree = ValDef(offsetSymbol.nn, staticOrFieldOff) + val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(containerName.mangledString))) + val offsetTree = ValDef(offsetSymbol.nn, getOffset.appliedTo(fieldTree)) info.defs = offsetTree :: info.defs case None => offsetSymbol = newSymbol(claz, offsetName(0), Synthetic, defn.LongType).enteredAfter(this) offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot)) - val offsetTree = ValDef(offsetSymbol.nn, staticOrFieldOff) + val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(containerName.mangledString))) + val offsetTree = ValDef(offsetSymbol.nn, getOffset.appliedTo(fieldTree)) appendOffsetDefs += (claz -> new OffsetInfo(List(offsetTree))) - - val waiting = requiredClass(s"$runtimeModule.${lazyNme.RLazyVals.waiting}") - val evaluating = Select(ref(helperModule), lazyNme.RLazyVals.evaluating) - val nullValue = Select(ref(helperModule), lazyNme.RLazyVals.nullValue) - val objCas = Select(ref(helperModule), lazyNme.RLazyVals.objCas) val offset = ref(offsetSymbol.nn) @@ -508,10 +537,8 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { else This(claz) - val methodSymbol = x.symbol.asTerm - val accessor = mkThreadSafeDefNew(methodSymbol, claz, containerSymbol, x.rhs, tpe, offset, objCas, - ref(waiting), evaluating, nullValue, swapOver) - Thicket(containerTree, accessor) + val (accessorDef, initMethodDef) = mkThreadSafeDef(x, claz, containerSymbol, offset, swapOver, helperModule, runtimeModule) + Thicket(containerTree, accessorDef, initMethodDef) } /** Create a threadsafe lazy accessor equivalent to such code @@ -545,7 +572,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { * } * ``` */ - def mkThreadSafeDefOld(methodSymbol: TermSymbol, + def mkThreadSafeDefLegacy(methodSymbol: TermSymbol, claz: ClassSymbol, ord: Int, target: Symbol, @@ -614,7 +641,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { DefDef(methodSymbol, loop) } - def transformMemberDefThreadSafeOld(x: ValOrDefDef)(using Context): Thicket = { + def transformMemberDefThreadSafeLegacy(x: ValOrDefDef)(using Context): Thicket = { val tpe = x.tpe.widen.resultType.widen val claz = x.symbol.owner.asClass val thizClass = Literal(Constant(claz.info)) @@ -658,7 +685,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { flag = ValDef(flagSymbol, Literal(Constant(0L))) val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(flagName.toString))) val offsetTree = ValDef(offsetSymbol.nn, getOffsetStatic.appliedTo(fieldTree)) - appendOffsetDefs += (claz -> new OffsetInfo(List(offsetTree), ord)) + oldAppendOffsetDefs += (claz -> new LegacyOffsetInfo(List(offsetTree), ord)) } val containerName = LazyLocalName.fresh(x.name.asTermName) @@ -673,7 +700,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { val state = Select(ref(helperModule), lazyNme.RLazyVals.state) val cas = Select(ref(helperModule), lazyNme.RLazyVals.cas) - val accessor = mkThreadSafeDefOld(x.symbol.asTerm, claz, ord, containerSymbol, x.rhs, tpe, offset, getFlag, state, cas, setFlag, wait) + val accessor = mkThreadSafeDefLegacy(x.symbol.asTerm, claz, ord, containerSymbol, x.rhs, tpe, offset, getFlag, state, cas, setFlag, wait) if (flag eq EmptyTree) Thicket(containerTree, accessor) else Thicket(containerTree, flag, accessor) @@ -683,28 +710,26 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { object LazyVals { val name: String = "lazyVals" val description: String = "expand lazy vals" - object lazyNme { import Names.TermName object RLazyVals { import scala.runtime.LazyVals.{Names => N} - val waiting: TermName = N.waiting.toTermName - val waitingAwaitRelease: TermName = N.waitingAwaitRelease.toTermName - val waitingRelease: TermName = N.waitingRelease.toTermName - val evaluating: TermName = N.evaluating.toTermName - val nullValue: TermName = N.nullValue.toTermName - val objCas: TermName = N.objCas.toTermName - val getOffset: TermName = N.getOffset.toTermName - val getStaticOffset: TermName = N.getStaticOffset.toTermName + val waiting: TermName = N.waiting.toTermName + val waitingAwaitRelease: TermName = N.waitingAwaitRelease.toTermName + val waitingRelease: TermName = N.waitingRelease.toTermName + val evaluating: TermName = N.evaluating.toTermName + val controlState: TermName = N.controlState.toTermName + val nullValue: TermName = N.nullValue.toTermName + val objCas: TermName = N.objCas.toTermName + val getStaticFieldOffset: TermName = N.getStaticFieldOffset.toTermName val get: TermName = N.get.toTermName val setFlag: TermName = N.setFlag.toTermName val wait4Notification: TermName = N.wait4Notification.toTermName val state: TermName = N.state.toTermName val cas: TermName = N.cas.toTermName val getOffset: TermName = N.getOffset.toTermName - val getOffsetStatic: TermName = "getOffsetStatic".toTermName + val getOffsetStatic: TermName = N.getOffsetStatic.toTermName val getDeclaredField: TermName = "getDeclaredField".toTermName - } val flag: TermName = "flag".toTermName val state: TermName = "state".toTermName diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 82413e2e6733..bbc62a8361e2 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -498,7 +498,6 @@ class TreeChecker extends Phase with SymTransformer { def isAllowed(sym: Symbol): Boolean = sym.is(ConstructorProxy) val symbolsNotDefined = (decls -- defined - constr.symbol).filterNot(isAllowed) - assert(symbolsNotDefined.isEmpty, i" $cls tree does not define members: ${symbolsNotDefined.toList}%, %\n" + i"expected: ${decls.toList}%, %\n" + diff --git a/compiler/test-resources/scripting/scriptPath.sc b/compiler/test-resources/scripting/scriptPath.sc index 837bd674aad8..46cd5e8a7385 100755 --- a/compiler/test-resources/scripting/scriptPath.sc +++ b/compiler/test-resources/scripting/scriptPath.sc @@ -16,8 +16,6 @@ val pathEntries = System.getenv("PATH").split(psep).toList System.err.printf("sun.java.command: %s\n", sys.props("sun.java.command")) System.err.printf("first 5 PATH entries:\n%s\n",pathEntries.take(5).mkString("\n")) - printf("script.path: %s\n",path.norm) - assert(path.endsWith("scriptPath.sc"),s"actual path [$path]") } extension(s: String) diff --git a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala index 3056f870d0fc..14a59ddaf88c 100644 --- a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala @@ -597,7 +597,7 @@ class DottyBytecodeTests extends DottyBytecodeTest { val clsIn = dir.lookupName("Test.class", directory = false).input val clsNode = loadClassNode(clsIn) val method = getMethod(clsNode, "test") - assertEquals(122, instructionsFromMethod(method).size) + assertEquals(118, instructionsFromMethod(method).size) } } diff --git a/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala b/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala index 710ceee0a7c0..639b04089abc 100644 --- a/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala +++ b/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala @@ -19,20 +19,25 @@ import dotty.tools.io.Directory import scala.io.Source import org.junit.Test - +import scala.util.Using +import java.io.File class PrintingTest { - def options(phase: String) = - List(s"-Xprint:$phase", "-color:never", "-classpath", TestConfiguration.basicClasspath) + def options(phase: String, flags: List[String]) = + List(s"-Xprint:$phase", "-color:never", "-classpath", TestConfiguration.basicClasspath) ::: flags private def compileFile(path: JPath, phase: String): Boolean = { val baseFilePath = path.toString.stripSuffix(".scala") val checkFilePath = baseFilePath + ".check" + val flagsFilePath = baseFilePath + ".flags" val byteStream = new ByteArrayOutputStream() val reporter = TestReporter.reporter(new PrintStream(byteStream), INFO) + val flags = + if (!(new File(flagsFilePath)).exists) Nil + else Using(Source.fromFile(flagsFilePath, StandardCharsets.UTF_8.name))(_.getLines().toList).get try { - Main.process((path.toString::options(phase)).toArray, reporter, null) + Main.process((path.toString :: options(phase, flags)).toArray, reporter, null) } catch { case e: Throwable => println(s"Compile $path exception:") @@ -63,4 +68,7 @@ class PrintingTest { @Test def untypedPrinting: Unit = testIn("tests/printing/untyped", "parser") + + @Test + def transformedPrinting: Unit = testIn("tests/printing/transformed", "repeatableAnnotations") } diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index 7cc80b95c297..7198f3060a5d 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -1,5 +1,7 @@ package scala.runtime +import java.util.concurrent.CountDownLatch + import scala.annotation.* /** @@ -41,40 +43,25 @@ object LazyVals { /* ------------- Start of public API ------------- */ + trait LazyValControlState + /** * Used to indicate the state of a lazy val that is being * evaluated and of which other threads await the result. */ - final class Waiting: - private var done = false - - /** - * Wakes up waiting threads. Called on completion of the evaluation - * of lazy val's right-hand side. - */ - def release(): Unit = synchronized { - done = true - notifyAll() - } - - /** - * Awaits the completion of the evaluation of lazy val's right-hand side. - */ - def awaitRelease(): Unit = synchronized { - while !done do wait() - } + final class Waiting extends CountDownLatch(1) with LazyValControlState /** * Used to indicate the state of a lazy val that is currently being * evaluated with no other thread awaiting its result. */ - object Evaluating + object Evaluating extends LazyValControlState /** * Used to indicate the state of a lazy val that has been evaluated to * `null`. */ - object NULL + object NullValue extends LazyValControlState final val BITS_PER_LAZY_VAL = 2L @@ -144,6 +131,7 @@ object LazyVals { unsafe.getLongVolatile(t, off) } + // kept for backward compatibility def getOffset(clz: Class[_], name: String): Long = { @nowarn val r = unsafe.objectFieldOffset(clz.getDeclaredField(name)) @@ -152,10 +140,10 @@ object LazyVals { r } - def getStaticOffset(clz: Class[_], name: String): Long = { - val r = unsafe.staticFieldOffset(clz.getDeclaredField(name)) + def getStaticFieldOffset(field: java.lang.reflect.Field): Long = { + val r = unsafe.staticFieldOffset(field) if (debug) - println(s"getStaticOffset($clz, $name) = $r") + println(s"getStaticFieldOffset(${field.getDeclaringClass}, ${field.getName}) = $r") r } @@ -168,11 +156,12 @@ object LazyVals { object Names { + final val controlState = "LazyValControlState" final val waiting = "Waiting" final val evaluating = "Evaluating" - final val nullValue = "NULL" - final val waitingAwaitRelease = "awaitRelease" - final val waitingRelease = "release" + final val nullValue = "NullValue" + final val waitingAwaitRelease = "await" + final val waitingRelease = "countDown" final val state = "STATE" final val cas = "CAS" final val objCas = "objCAS" @@ -180,6 +169,7 @@ object LazyVals { final val wait4Notification = "wait4Notification" final val get = "get" final val getOffset = "getOffset" - final val getStaticOffset = "getStaticOffset" + final val getOffsetStatic = "getOffsetStatic" + final val getStaticFieldOffset = "getStaticFieldOffset" } } diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 6dd29cbe8adb..e548a5964cac 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -12,6 +12,9 @@ | Access non-initialized value num1. Promotion trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] | ^^^^ +-- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ +5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Cannot prove the method argument is hot. Only hot values are safe to leak. | Found = Fun { this = ThisRef[object Test], owner = object Test }. Calling trace: | -> object Test { [ t3273.scala:3 ] diff --git a/tests/printing/transformed/lazy-vals-legacy.check b/tests/printing/transformed/lazy-vals-legacy.check new file mode 100644 index 000000000000..a08148e441d6 --- /dev/null +++ b/tests/printing/transformed/lazy-vals-legacy.check @@ -0,0 +1,59 @@ +[[syntax trees at end of MegaPhase{dropOuterAccessors, checkNoSuperThis, flatten, transformWildcards, moveStatic, expandPrivate, restoreScopes, selectStatic, Collect entry points, collectSuperCalls, repeatableAnnotations}]] // tests/printing/transformed/lazy-vals-legacy.scala +package { + @SourceFile("tests/printing/transformed/lazy-vals-legacy.scala") final module + class + A extends Object { + def (): Unit = + { + super() + () + } + @static private def (): Unit = + { + A.OFFSET$_m_0 = + scala.runtime.LazyVals.getOffsetStatic( + classOf[Object {...}].getDeclaredField("0bitmap$1") + ) + () + } + @static @static val OFFSET$_m_0: Long = + scala.runtime.LazyVals.getOffsetStatic( + classOf[Object {...}].getDeclaredField("0bitmap$1") + ) + lazy var 0bitmap$1: Long = 0L + private def writeReplace(): Object = + new scala.runtime.ModuleSerializationProxy(classOf[A]) + lazy var x$lzy1: Int = 0 + lazy def x(): Int = + while do + { + val flag: Long = scala.runtime.LazyVals.get(this, A.OFFSET$_m_0) + val state: Long = scala.runtime.LazyVals.STATE(flag, 0) + if state.==(3) then return A.x$lzy1 else + if state.==(0) then + if scala.runtime.LazyVals.CAS(this, A.OFFSET$_m_0, flag, 1, 0) + then + + try + { + val result: Int = 2 + A.x$lzy1 = result + scala.runtime.LazyVals.setFlag(this, A.OFFSET$_m_0, 3, 0) + return result + } + catch + { + case ex @ ex => + scala.runtime.LazyVals.setFlag(this, A.OFFSET$_m_0, 0, 0) + throw ex + } + else () + else + scala.runtime.LazyVals.wait4Notification(this, A.OFFSET$_m_0, flag + , + 0) + } + } + final lazy module val A: A = new A() +} + diff --git a/tests/printing/transformed/lazy-vals-legacy.flags b/tests/printing/transformed/lazy-vals-legacy.flags new file mode 100644 index 000000000000..a817c9c65fec --- /dev/null +++ b/tests/printing/transformed/lazy-vals-legacy.flags @@ -0,0 +1 @@ +-Xlegacy-lazy-values \ No newline at end of file diff --git a/tests/printing/transformed/lazy-vals-legacy.scala b/tests/printing/transformed/lazy-vals-legacy.scala new file mode 100644 index 000000000000..28387acec9c5 --- /dev/null +++ b/tests/printing/transformed/lazy-vals-legacy.scala @@ -0,0 +1,3 @@ +object A { + lazy val x: Int = 2 +} \ No newline at end of file diff --git a/tests/printing/transformed/lazy-vals-new.check b/tests/printing/transformed/lazy-vals-new.check new file mode 100644 index 000000000000..8c574cf8b895 --- /dev/null +++ b/tests/printing/transformed/lazy-vals-new.check @@ -0,0 +1,83 @@ +[[syntax trees at end of MegaPhase{dropOuterAccessors, checkNoSuperThis, flatten, transformWildcards, moveStatic, expandPrivate, restoreScopes, selectStatic, Collect entry points, collectSuperCalls, repeatableAnnotations}]] // tests/printing/transformed/lazy-vals-new.scala +package { + @SourceFile("tests/printing/transformed/lazy-vals-new.scala") final module + class + A extends Object { + def (): Unit = + { + super() + () + } + @static private def (): Unit = + { + A.OFFSET$_m_0 = + scala.runtime.LazyVals.getStaticFieldOffset( + classOf[Object {...}].getDeclaredField("x$lzy1") + ) + () + } + @static @static val OFFSET$_m_0: Long = + scala.runtime.LazyVals.getStaticFieldOffset( + classOf[Object {...}].getDeclaredField("x$lzy1") + ) + private def writeReplace(): Object = + new scala.runtime.ModuleSerializationProxy(classOf[A]) + @volatile lazy var x$lzy1: Object = null + lazy def x(): Int = + while do + { + val current: Object = A#x$lzy1 + if current.==(null) then + if + scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, null, + scala.runtime.LazyVals.Evaluating + ) + then + { + var result: Object = null + try + { + result = scala.Int.box(2) + if result.==(null) then + result = scala.runtime.LazyVals.NullValue + } + finally + if + scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, + scala.runtime.LazyVals.Evaluating + , result).unary_!() + then + { + val lock: scala.runtime.LazyVals.LazyVals$Waiting = + A#x$lzy1.asInstanceOf[ + scala.runtime.LazyVals.LazyVals$Waiting + ] + scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, + lock + , result) + lock.countDown() + } + } + else + if current.isInstanceOf[scala.runtime.LazyVals.Evaluating] then + { + scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, + scala.runtime.LazyVals.Evaluating + , new scala.runtime.LazyVals.LazyVals$Waiting()) + () + } + else + if current.isInstanceOf[scala.runtime.LazyVals.NullValue] then + return 0 + else + if current.isInstanceOf[scala.runtime.LazyVals.LazyVals$Waiting] + then + + current.asInstanceOf[scala.runtime.LazyVals.LazyVals$Waiting]. + await + () + else return scala.Int.unbox(current) + } + } + final lazy module val A: A = new A() +} \ No newline at end of file diff --git a/tests/printing/transformed/lazy-vals-new.scala b/tests/printing/transformed/lazy-vals-new.scala new file mode 100644 index 000000000000..28387acec9c5 --- /dev/null +++ b/tests/printing/transformed/lazy-vals-new.scala @@ -0,0 +1,3 @@ +object A { + lazy val x: Int = 2 +} \ No newline at end of file diff --git a/tests/run/lazyVals-legacy.check b/tests/run/lazyVals-legacy.check new file mode 100644 index 000000000000..aacc0b983ab8 --- /dev/null +++ b/tests/run/lazyVals-legacy.check @@ -0,0 +1,2 @@ +42 +bar diff --git a/tests/run/lazyVals-legacy.flags b/tests/run/lazyVals-legacy.flags new file mode 100644 index 000000000000..a817c9c65fec --- /dev/null +++ b/tests/run/lazyVals-legacy.flags @@ -0,0 +1 @@ +-Xlegacy-lazy-values \ No newline at end of file diff --git a/tests/run/lazyVals-legacy.scala b/tests/run/lazyVals-legacy.scala new file mode 100644 index 000000000000..f151ed5fe6ce --- /dev/null +++ b/tests/run/lazyVals-legacy.scala @@ -0,0 +1,12 @@ +object Test { + def foo = { + lazy val s = 42 + s + } + + lazy val bar = "bar" + + def main(args: Array[String]): Unit = + println(foo) + println(bar) +} diff --git a/tests/run/serialization-new.check b/tests/run/serialization-new.check index 93dd1dc4fcce..65b6f809d51f 100644 --- a/tests/run/serialization-new.check +++ b/tests/run/serialization-new.check @@ -224,7 +224,14 @@ x = Paul y = Paul x equals y: true, y equals x: true +Calculating a1 1 +Calculating a2 2 +Calculating a3 +3 +Calculating a1 1 +Calculating a2 2 +3 \ No newline at end of file diff --git a/tests/run/serialization-new.scala b/tests/run/serialization-new.scala index 786fc12bd066..202920964479 100644 --- a/tests/run/serialization-new.scala +++ b/tests/run/serialization-new.scala @@ -470,14 +470,26 @@ object Test7 { // Verify that transient lazy vals don't get serialized class WithTransient extends Serializable { - @transient lazy val a1 = 1 - @transient private lazy val a2 = 2 + @transient lazy val a1 = { + println("Calculating a1") + 1 + } + @transient private lazy val a2 = { + println("Calculating a2") + 2 + } + private lazy val a3 = { + println("Calculating a3") + 3 + } + @transient object B extends Serializable @transient private object C extends Serializable def test = { println(a1) println(a2) + println(a3) if (B == null || C == null) println("Transient nested object failed to serialize properly") } From 5442089363e76bd7cd4ecfd9af335e09f800548f Mon Sep 17 00:00:00 2001 From: Szymon Rodziewicz Date: Tue, 9 Aug 2022 17:55:12 +0200 Subject: [PATCH 3/7] Benchmarking lazy vals --- .../dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala | 0 .../scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala | 0 .../src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala | 0 .../tools/benchmarks/lazyvals/UninitializedAccess copy.scala | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {bench-run => bench-micro}/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala (100%) rename {bench-run => bench-micro}/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala (100%) rename {bench-run => bench-micro}/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala (100%) rename {bench-run => bench-micro}/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala (100%) diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala similarity index 100% rename from bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala rename to bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/ContendedInitialization.scala diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala similarity index 100% rename from bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala rename to bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala similarity index 100% rename from bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala rename to bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala diff --git a/bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala similarity index 100% rename from bench-run/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala rename to bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala From 81094bef19fde839107761ce34e0fdb6a71100b8 Mon Sep 17 00:00:00 2001 From: Szymon Rodziewicz Date: Mon, 22 Aug 2022 11:26:24 +0200 Subject: [PATCH 4/7] Optimizing performance of LazyVals --- bench-micro/results_isStable.json | 0 .../lazyvals/InitializedAccess.scala | 21 ++++---- .../lazyvals/InitializedAccessAny.scala | 30 +++++++++++ .../lazyvals/InitializedAccessGeneric.scala | 30 +++++++++++ .../lazyvals/InitializedAccessMultiple.scala | 34 +++++++++++++ .../lazyvals/InitializedAccessString.scala | 30 +++++++++++ .../tools/benchmarks/lazyvals/LazyVals.scala | 50 ++++++++++++++++++- ...s copy.scala => UninitializedAccess.scala} | 10 ++-- .../UninitializedAccessMultiple.scala | 27 ++++++++++ .../dotty/tools/dotc/transform/LazyVals.scala | 15 ++++-- library/src/scala/runtime/LazyVals.scala | 2 +- 11 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 bench-micro/results_isStable.json create mode 100644 bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala create mode 100644 bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala create mode 100644 bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala create mode 100644 bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala rename bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/{UninitializedAccess copy.scala => UninitializedAccess.scala} (82%) create mode 100644 bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala diff --git a/bench-micro/results_isStable.json b/bench-micro/results_isStable.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala index a00419d5356b..88053733138e 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala @@ -14,16 +14,17 @@ import java.util.concurrent.TimeUnit @State(Scope.Benchmark) class InitializedAccess { - var holder: LazyHolder = _ + var holder: LazyHolder = _ - @Setup - def prepare: Unit = { - holder = new LazyHolder - holder.value - } + @Setup + def prepare: Unit = { + holder = new LazyHolder + holder.value + } - @Benchmark - def measureInitialized(bh: Blackhole) = { - bh.consume(holder.value) - } + @Benchmark + def measureInitialized(bh: Blackhole) = { + bh.consume(holder) + bh.consume(holder.value) + } } \ No newline at end of file diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala new file mode 100644 index 000000000000..3f6e83fec9a8 --- /dev/null +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala @@ -0,0 +1,30 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyAnyHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class InitializedAccessAny { + + var holder: LazyAnyHolder = _ + + @Setup + def prepare: Unit = { + holder = new LazyAnyHolder + holder.value + } + + @Benchmark + def measureInitialized(bh: Blackhole) = { + bh.consume(holder) + bh.consume(holder.value) + } +} \ No newline at end of file diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala new file mode 100644 index 000000000000..918611e8da75 --- /dev/null +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala @@ -0,0 +1,30 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyGenericHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class InitializedAccessGeneric { + + var holder: LazyGenericHolder[String] = _ + + @Setup + def prepare: Unit = { + holder = new LazyGenericHolder[String]("foo") + holder.value + } + + @Benchmark + def measureInitialized(bh: Blackhole) = { + bh.consume(holder) + bh.consume(holder.value) + } +} \ No newline at end of file diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala new file mode 100644 index 000000000000..257e0a546cd7 --- /dev/null +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala @@ -0,0 +1,34 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class InitializedAccessMultiple { + + var holders: Array[LazyHolder] = _ + + @Setup + def prepare: Unit = { + holders = Array.fill(100){ new LazyHolder } + } + + @Benchmark + def measureInitialized(bh: Blackhole) = { + var i = 0 + while(i < 100) { + val currentHolder = holders(i) + bh.consume(currentHolder) + bh.consume(currentHolder.value) + i = i + 1 + } + } +} \ No newline at end of file diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala new file mode 100644 index 000000000000..06234df8da44 --- /dev/null +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala @@ -0,0 +1,30 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyStringHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class InitializedAccessString { + + var holder: LazyStringHolder = _ + + @Setup + def prepare: Unit = { + holder = new LazyStringHolder + holder.value + } + + @Benchmark + def measureInitialized(bh: Blackhole) = { + bh.consume(holder) + bh.consume(holder.value) + } +} \ No newline at end of file diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala index fffddca41648..527f2302b762 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala @@ -1,7 +1,53 @@ package dotty.tools.benchmarks.lazyvals - +import java.util.concurrent.CountDownLatch object LazyVals { + + trait Foo + class Bar1 extends Foo + class Bar2 extends Foo + class Bar3 extends Foo + class Bar4 extends Foo + class Bar5 extends Bar4 + + class LazyStringHolder { + + lazy val value: String = { + System.nanoTime() % 5 match { + case 0 => "abc" + case 1 => "def" + case 2 => "ghi" + case 3 => "jkl" + case 4 => "mno" + } + } + } + class LazyHolder { - lazy val value = 1 + + lazy val value: List[Int] = { + System.nanoTime() % 5 match { + case 0 => 1 :: 2 :: Nil + case 1 => Nil + case 2 => 1 :: Nil + case 3 => Nil + case 4 => 1 :: 2 :: 3 :: Nil + } + } + } + + class LazyGenericHolder[A](v: => A) { + lazy val value: A = v + } + + class LazyAnyHolder { + lazy val value: Any = { + System.nanoTime() % 5 match { + case 0 => new Bar1 + case 1 => new Bar2 + case 2 => new Bar3 + case 3 => new Bar4 + case 4 => new Bar4 + } + } } } \ No newline at end of file diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess.scala similarity index 82% rename from bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala rename to bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess.scala index c7d6eafccf9f..5f90697cb8a7 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess copy.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess.scala @@ -14,16 +14,12 @@ import java.util.concurrent.TimeUnit @State(Scope.Benchmark) class UninitializedAccess { - var holder: LazyHolder = _ - - @Setup - def prepare: Unit = { - holder = new LazyHolder - } - @Benchmark def measureInitialized(bh: Blackhole) = { + var i = 0 + val holder = new LazyHolder bh.consume(holder) bh.consume(holder.value) + i = i + 1 } } \ No newline at end of file diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala new file mode 100644 index 000000000000..6f72d64423cf --- /dev/null +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala @@ -0,0 +1,27 @@ +package dotty.tools.benchmarks.lazyvals + +import org.openjdk.jmh.annotations._ +import LazyVals.LazyHolder +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class UninitializedAccessMultiple { + + @Benchmark + def measureInitialized(bh: Blackhole) = { + var i = 0 + while(i < 100) { + val holder = new LazyHolder + bh.consume(holder) + bh.consume(holder.value) + i = i + 1 + } + } +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index 9ce093174447..b8f0998b8551 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -390,12 +390,19 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { val rhs = memberDef.rhs val rhsMappedOwner = rhs.changeOwnerAfter(memberDef.symbol, lazyInitMethodSymbol, this) val valueSymbol = newSymbol(accessorMethodSymbol, lazyNme.result, Synthetic | Mutable, defn.ObjectType) + + val immediateValueCondition = + if (controlState.tpe <:< tp) then + ref(valueSymbol).select(defn.Any_!=).appliedTo(nullLiteral).select(nme.And).appliedTo(ref(valueSymbol).select(defn.Any_isInstanceOf).appliedToTypeTree(controlState) + .select(nme.UNARY_!).appliedToNone) + else + ref(valueSymbol).select(defn.Any_isInstanceOf).appliedToTypeTree(TypeTree(tp)) + val accessorBody = Block( ValDef(valueSymbol, ref(target)) :: Nil, If( // if _x != null && !_x.isInstanceOf[LazyValControlState] then - ref(valueSymbol).select(defn.Any_!=).appliedTo(nullLiteral).select(nme.And).appliedTo(ref(valueSymbol).select(defn.Any_isInstanceOf).appliedToTypeTree(controlState) - .select(nme.UNARY_!).appliedToNone), + immediateValueCondition, Return(ref(valueSymbol).ensureConforms(tp), accessorMethodSymbol), // then return _x.asInstanceOf[A] If( ref(valueSymbol).select(defn.Any_==).appliedTo(nullValue), @@ -419,11 +426,11 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { ref(lockSymb).select(lazyNme.RLazyVals.waitingRelease).ensureApplied) } // finally block - val fin = If( + val fin = Block(If( objCasFlag.appliedTo(thiz, offset, evaluating, ref(resSymb)).select(nme.UNARY_!).appliedToNone, lockRel, unitLiteral - ).withType(defn.UnitType) + ) :: Nil, Return(ref(resSymb), lazyInitMethodSymbol)).withType(defn.UnitType) // entire try block val evaluate = Try( diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index 7198f3060a5d..410737c10c07 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -43,7 +43,7 @@ object LazyVals { /* ------------- Start of public API ------------- */ - trait LazyValControlState + sealed trait LazyValControlState /** * Used to indicate the state of a lazy val that is being From 937424a619499ea6f3e161d6bd76631f23b99b73 Mon Sep 17 00:00:00 2001 From: Szymon Rodziewicz Date: Mon, 19 Sep 2022 16:58:09 +0200 Subject: [PATCH 5/7] Fix failing testCompilation tests for new lazy vals Pre-review lazy vals adjustments --- .../lazyvals/InitializedAccess.scala | 2 +- .../lazyvals/InitializedAccessAny.scala | 2 +- .../lazyvals/InitializedAccessGeneric.scala | 2 +- .../lazyvals/InitializedAccessMultiple.scala | 2 +- .../lazyvals/InitializedAccessString.scala | 2 +- .../tools/benchmarks/lazyvals/LazyVals.scala | 2 +- .../lazyvals/UninitializedAccess.scala | 2 +- .../UninitializedAccessMultiple.scala | 2 +- .../dotty/tools/dotc/ast/TreeTypeMap.scala | 1 - .../tools/dotc/config/ScalaSettings.scala | 2 +- .../dotty/tools/dotc/transform/LazyVals.scala | 49 +- .../test/dotc/pos-lazy-vals-tests.allowlist | 37 ++ compiler/test/dotc/run-from-tasty.blacklist | 2 +- .../test/dotc/run-lazy-vals-tests.allowlist | 66 +++ .../test/dotc/run-test-pickling.blacklist | 2 +- compiler/test/dotc/run-test-recheck.excludes | 2 +- compiler/test/dotty/tools/TestSources.scala | 13 +- .../backend/jvm/DottyBytecodeTests.scala | 2 +- .../dotty/tools/dotc/CompilationTests.scala | 6 +- library/src/scala/runtime/LazyVals.scala | 7 + project/MiMaFilters.scala | 9 +- .../transformed/lazy-vals-legacy.flags | 1 - .../printing/transformed/lazy-vals-new.check | 79 ++- .../printing/transformed/lazy-vals-new.flags | 1 + .../stdlibExperimentalDefinitions.scala | 10 + tests/run/lazyVals-legacy.check | 2 - tests/run/lazyVals-legacy.flags | 1 - tests/run/lazyVals-legacy.scala | 12 - tests/run/serialization-new-legacy.check | 226 ++++++++ tests/run/serialization-new-legacy.scala | 483 ++++++++++++++++++ 30 files changed, 942 insertions(+), 87 deletions(-) create mode 100644 compiler/test/dotc/pos-lazy-vals-tests.allowlist create mode 100644 compiler/test/dotc/run-lazy-vals-tests.allowlist delete mode 100644 tests/printing/transformed/lazy-vals-legacy.flags create mode 100644 tests/printing/transformed/lazy-vals-new.flags delete mode 100644 tests/run/lazyVals-legacy.check delete mode 100644 tests/run/lazyVals-legacy.flags delete mode 100644 tests/run/lazyVals-legacy.scala create mode 100644 tests/run/serialization-new-legacy.check create mode 100644 tests/run/serialization-new-legacy.scala diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala index 88053733138e..a2aaf3e88570 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccess.scala @@ -27,4 +27,4 @@ class InitializedAccess { bh.consume(holder) bh.consume(holder.value) } -} \ No newline at end of file +} diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala index 3f6e83fec9a8..5a6b4ae1686d 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessAny.scala @@ -27,4 +27,4 @@ class InitializedAccessAny { bh.consume(holder) bh.consume(holder.value) } -} \ No newline at end of file +} diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala index 918611e8da75..a95cb1de2980 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessGeneric.scala @@ -27,4 +27,4 @@ class InitializedAccessGeneric { bh.consume(holder) bh.consume(holder.value) } -} \ No newline at end of file +} diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala index 257e0a546cd7..4f3c75fd920b 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessMultiple.scala @@ -31,4 +31,4 @@ class InitializedAccessMultiple { i = i + 1 } } -} \ No newline at end of file +} diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala index 06234df8da44..25cc0f9b288d 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/InitializedAccessString.scala @@ -27,4 +27,4 @@ class InitializedAccessString { bh.consume(holder) bh.consume(holder.value) } -} \ No newline at end of file +} diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala index 527f2302b762..0afd93d086be 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/LazyVals.scala @@ -50,4 +50,4 @@ object LazyVals { } } } -} \ No newline at end of file +} diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess.scala index 5f90697cb8a7..417d22f67d48 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccess.scala @@ -22,4 +22,4 @@ class UninitializedAccess { bh.consume(holder.value) i = i + 1 } -} \ No newline at end of file +} diff --git a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala index 6f72d64423cf..133a0932bf51 100644 --- a/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala +++ b/bench-micro/src/main/scala/dotty/tools/benchmarks/lazyvals/UninitializedAccessMultiple.scala @@ -24,4 +24,4 @@ class UninitializedAccessMultiple { i = i + 1 } } -} \ No newline at end of file +} diff --git a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala index 51d3aba8a2b1..71998aff9304 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala @@ -7,7 +7,6 @@ import Types._, Contexts._, Flags._ import Symbols._, Annotations._, Trees._, Symbols._, Constants.Constant import Decorators._ import dotty.tools.dotc.transform.SymUtils._ -import transform.LazyVals /** A map that applies three functions and a substitution together to a tree and * makes sure they are coordinated so that the result is well-typed. The functions are diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 52461f5f9c80..09bedd3e8b35 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -241,7 +241,6 @@ private sealed trait XSettings: val XcheckMacros: Setting[Boolean] = BooleanSetting("-Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros")) val XmainClass: Setting[String] = StringSetting("-Xmain-class", "path", "Class for manifest's Main-Class entry (only useful with -d )", "") val XimplicitSearchLimit: Setting[Int] = IntSetting("-Ximplicit-search-limit", "Maximal number of expressions to be generated in an implicit search", 50000) - val XlegacyLazyVals: Setting[Boolean] = BooleanSetting("-Xlegacy-lazy-values", "Use legacy lazy vals implementations") val XmixinForceForwarders = ChoiceSetting( name = "-Xmixin-force-forwarders", @@ -331,6 +330,7 @@ private sealed trait YSettings: val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only)") val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references") val YccNoAbbrev: Setting[Boolean] = BooleanSetting("-Ycc-no-abbrev", "Used in conjunction with captureChecking language import, suppress type abbreviations") + val YlightweightLazyVals: Setting[Boolean] = BooleanSetting("-Ylightweight-lazy-vals", "Use experimental lightweight implementation of lazy vals") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index b8f0998b8551..d8f4cbffb623 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -2,21 +2,22 @@ package dotty.tools.dotc package transform import java.util.IdentityHashMap - import ast.tpd import core.Annotations.Annotation import core.Constants.Constant -import core.Contexts._ -import core.Decorators._ +import core.Contexts.* +import core.Decorators.* import core.DenotTransformers.IdentityDenotTransformer -import core.Flags._ -import core.NameKinds.{LazyBitMapName, LazyLocalInitName, LazyLocalName, ExpandedName} +import core.Flags.* +import core.NameKinds.{ExpandedName, LazyBitMapName, LazyLocalInitName, LazyLocalName} import core.StdNames.nme -import core.Symbols._ -import core.Types._ +import core.Symbols.* +import core.Types.* import core.{Names, StdNames} +import dotty.tools.dotc.config.Feature import transform.MegaPhase.MiniPhase -import transform.SymUtils._ +import transform.SymUtils.* + import scala.collection.mutable class LazyVals extends MiniPhase with IdentityDenotTransformer { @@ -59,9 +60,6 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { else nullables.toList } - private inline def isLegacyLazyVals(using ctx: Context): Boolean = - ctx.settings.XlegacyLazyVals.value - private def needsBoxing(tp: Type)(using Context): Boolean = tp != NoType && tp != defn.UnitType && tp.classSymbol.isPrimitiveValueClass private def boxIfCan(tp: Type)(using Context): Type = @@ -120,7 +118,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { */ override def transformTemplate(template: Template)(using Context): Tree = { val cls = ctx.owner.asClass - (if isLegacyLazyVals then oldAppendOffsetDefs else appendOffsetDefs).get(cls) match { + (if !ctx.settings.YlightweightLazyVals.value then oldAppendOffsetDefs else appendOffsetDefs).get(cls) match { case None => template case Some(data) => data.defs.foreach(_.symbol.addAnnotation(Annotation(defn.ScalaStaticAnnot))) @@ -338,10 +336,15 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { * val current: AnyRef = _x * if current == null then * if CAS(_x, null, Evaluating) then + * var resultNullable: AnyRef = null * var result: AnyRef = null * try + * resultNullable = rhs * nullable = null - * if result == null then result = NullValue + * if resultNullable == null then + * result = NullValue + * else + * result = resultNullable * return result * finally * if !CAS(_x, Evaluating, result) then @@ -417,6 +420,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { // if observed a null (uninitialized) value val initialize = { // var result: AnyRef + val resSymbNullable = newSymbol(lazyInitMethodSymbol, lazyNme.resultNullable, Synthetic | Mutable, defn.ObjectType) val resSymb = newSymbol(lazyInitMethodSymbol, lazyNme.result, Synthetic | Mutable, defn.ObjectType) // releasing block in finally val lockRel = { @@ -430,26 +434,26 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { objCasFlag.appliedTo(thiz, offset, evaluating, ref(resSymb)).select(nme.UNARY_!).appliedToNone, lockRel, unitLiteral - ) :: Nil, Return(ref(resSymb), lazyInitMethodSymbol)).withType(defn.UnitType) + ) :: Nil, unitLiteral).withType(defn.UnitType) // entire try block val evaluate = Try( Block( - (Assign(ref(resSymb), if needsBoxing(tp) && rhsMappedOwner != EmptyTree then rhsMappedOwner.ensureConforms(boxIfCan(tp)) else rhsMappedOwner) // try result = rhs + (Assign(ref(resSymbNullable), if needsBoxing(tp) && rhsMappedOwner != EmptyTree then rhsMappedOwner.ensureConforms(boxIfCan(tp)) else rhsMappedOwner) // try result = rhs :: If( - ref(resSymb).select(defn.Any_==).appliedTo(nullLiteral), + ref(resSymbNullable).select(defn.Any_==).appliedTo(nullLiteral), Assign(ref(resSymb), nullValue), - unitLiteral + Assign(ref(resSymb), ref(resSymbNullable)) ) :: Nil) ::: nullOut(nullableFor(accessorMethodSymbol)), - unitLiteral), + Return(ref(resSymbNullable), lazyInitMethodSymbol)), Nil, fin ) // if CAS(_, null, Evaluating) If( objCasFlag.appliedTo(thiz, offset, nullLiteral, evaluating), - Block(ValDef(resSymb, nullLiteral) :: Nil, // var result: AnyRef = null + Block(ValDef(resSymb, nullLiteral) :: ValDef(resSymbNullable, nullLiteral) :: Nil, // var result: AnyRef = null evaluate), // try ... finally ... unitLiteral ).withType(defn.UnitType) @@ -490,10 +494,10 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { assert(!(x.symbol is Mutable)) - if isLegacyLazyVals then - transformMemberDefThreadSafeLegacy(x) - else + if ctx.settings.YlightweightLazyVals.value then transformMemberDefThreadSafeNew(x) + else + transformMemberDefThreadSafeLegacy(x) } def transformMemberDefThreadSafeNew(x: ValOrDefDef)(using Context): Thicket = { @@ -741,6 +745,7 @@ object LazyVals { val flag: TermName = "flag".toTermName val state: TermName = "state".toTermName val result: TermName = "result".toTermName + val resultNullable: TermName = "resultNullable".toTermName val value: TermName = "value".toTermName val initialized: TermName = "initialized".toTermName val initialize: TermName = "initialize".toTermName diff --git a/compiler/test/dotc/pos-lazy-vals-tests.allowlist b/compiler/test/dotc/pos-lazy-vals-tests.allowlist new file mode 100644 index 000000000000..227cc687b9db --- /dev/null +++ b/compiler/test/dotc/pos-lazy-vals-tests.allowlist @@ -0,0 +1,37 @@ +Repeated.scala +byname-implicits-8.scala +existentials.scala +i1235.scala +i13332super.scala +i13349.scala +i13460.scala +i14626.scala +i1753.scala +i4031.scala +i4328.scala +i6450.scala +i6565.scala +i8031.scala +i8111.scala +i8900-unflip.scala +lazyvals.scala +singletons.scala +spec-traits.scala +spurious-overload.scala +t1591_pos.scala +t2910.scala +t3411.scala +t3420.scala +t3452f.scala +t3670.scala +t3927.scala +t4432.scala +t4716.scala +t4717.scala +t5099.scala +t5796.scala +t6278-synth-def.scala +t6925b.scala +t7011.scala +t8306.scala +zipped.scala \ No newline at end of file diff --git a/compiler/test/dotc/run-from-tasty.blacklist b/compiler/test/dotc/run-from-tasty.blacklist index 2c483e9e34b6..b6266125761a 100644 --- a/compiler/test/dotc/run-from-tasty.blacklist +++ b/compiler/test/dotc/run-from-tasty.blacklist @@ -1,2 +1,2 @@ # CI only: cannot reduce summonFrom with -sip23-valueof.scala +sip23-valueof.scala \ No newline at end of file diff --git a/compiler/test/dotc/run-lazy-vals-tests.allowlist b/compiler/test/dotc/run-lazy-vals-tests.allowlist new file mode 100644 index 000000000000..2edc6c1050d7 --- /dev/null +++ b/compiler/test/dotc/run-lazy-vals-tests.allowlist @@ -0,0 +1,66 @@ +IArrayOps.scala +Lazies1.scala +Lazies2.scala +OrderingTest.scala +anon-mirror-gen-local.scala +byname-implicits-28.scala +byname-implicits-30.scala +byname-implicits-5.scala +exports.scala +i13146.scala +i13146a.scala +i13332a.scala +i13332shapeless.scala +i13358.scala +i1692.scala +i1692b.scala +i1856.scala +i2266.scala +i2275.scala +i4451.scala +i4559.scala +i5340.scala +i5350.scala +i7675.scala +i9473.scala +isInstanceOf-eval.scala +lazy-exprs.scala +lazy-impl.scala +lazy-implicit-lists.scala +lazy-override-run.scala +lazy-traits.scala +lazyVals.scala +lazyVals_c3.0.0.scala +lazyVals_c3.1.0.scala +nonLocalReturns.scala +nothing-lazy-val.scala +null-lazy-val.scala +patmatch-classtag.scala +priorityQueue.scala +serialization-new-legacy.scala +serialization-new.scala +singletons.scala +statics.scala +stream_flatmap_odds.scala +t1535.scala +t1591.scala +t2333.scala +t3038.scala +t3670.scala +t3699.scala +t3877.scala +t3895.scala +t3980.scala +t429.scala +t5552.scala +t5610a.scala +t603.scala +t6272.scala +t6443-by-name.scala +t6443-varargs.scala +t704.scala +t7406.scala +t8245.scala +unapply.scala +unit-lazy-val.scala +view-iterator-stream.scala \ No newline at end of file diff --git a/compiler/test/dotc/run-test-pickling.blacklist b/compiler/test/dotc/run-test-pickling.blacklist index 9f19b439135c..bd74832cf6a6 100644 --- a/compiler/test/dotc/run-test-pickling.blacklist +++ b/compiler/test/dotc/run-test-pickling.blacklist @@ -43,4 +43,4 @@ i12753 t6138 t6138-2 i12656.scala -trait-static-forwarder +trait-static-forwarder \ No newline at end of file diff --git a/compiler/test/dotc/run-test-recheck.excludes b/compiler/test/dotc/run-test-recheck.excludes index de2fb3f8aed8..d6a5d0f2d594 100644 --- a/compiler/test/dotc/run-test-recheck.excludes +++ b/compiler/test/dotc/run-test-recheck.excludes @@ -10,4 +10,4 @@ i5976.scala tagless.scala safeThrowsStrawman2.scala t7584.scala -function-arity.scala +function-arity.scala \ No newline at end of file diff --git a/compiler/test/dotty/tools/TestSources.scala b/compiler/test/dotty/tools/TestSources.scala index c4d36b16c90b..6961a61b69b6 100644 --- a/compiler/test/dotty/tools/TestSources.scala +++ b/compiler/test/dotty/tools/TestSources.scala @@ -13,21 +13,26 @@ object TestSources { def posFromTastyBlacklistFile: String = "compiler/test/dotc/pos-from-tasty.blacklist" def posTestPicklingBlacklistFile: String = "compiler/test/dotc/pos-test-pickling.blacklist" - def posTestRecheckExcludesFile = "compiler/test/dotc/pos-test-recheck.excludes" + def posTestRecheckExcludesFile: String = "compiler/test/dotc/pos-test-recheck.excludes" + def posLazyValsAllowlistFile: String = "compiler/test/dotc/pos-lazy-vals-tests.allowlist" def posFromTastyBlacklisted: List[String] = loadList(posFromTastyBlacklistFile) def posTestPicklingBlacklisted: List[String] = loadList(posTestPicklingBlacklistFile) - def posTestRecheckExcluded = loadList(posTestRecheckExcludesFile) + def posTestRecheckExcluded: List[String] = loadList(posTestRecheckExcludesFile) + def posLazyValsAllowlist: List[String] = loadList(posLazyValsAllowlistFile) // run tests lists def runFromTastyBlacklistFile: String = "compiler/test/dotc/run-from-tasty.blacklist" def runTestPicklingBlacklistFile: String = "compiler/test/dotc/run-test-pickling.blacklist" - def runTestRecheckExcludesFile = "compiler/test/dotc/run-test-recheck.excludes" + def runTestRecheckExcludesFile: String = "compiler/test/dotc/run-test-recheck.excludes" + def runLazyValsAllowlistFile: String = "compiler/test/dotc/run-lazy-vals-tests.allowlist" + def runFromTastyBlacklisted: List[String] = loadList(runFromTastyBlacklistFile) def runTestPicklingBlacklisted: List[String] = loadList(runTestPicklingBlacklistFile) - def runTestRecheckExcluded = loadList(runTestRecheckExcludesFile) + def runTestRecheckExcluded: List[String] = loadList(runTestRecheckExcludesFile) + def runLazyValsAllowlist: List[String] = loadList(runLazyValsAllowlistFile) // load lists diff --git a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala index 14a59ddaf88c..2c618ea91e96 100644 --- a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala @@ -597,7 +597,7 @@ class DottyBytecodeTests extends DottyBytecodeTest { val clsIn = dir.lookupName("Test.class", directory = false).input val clsNode = loadClassNode(clsIn) val method = getMethod(clsNode, "test") - assertEquals(118, instructionsFromMethod(method).size) + assertEquals(88, instructionsFromMethod(method).size) } } diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 915e4e5f2e50..261e6af21927 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -43,6 +43,8 @@ class CompilationTests { compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")), compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init")), + // Run tests for experimental lightweight lazy vals + compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init", "-Ylightweight-lazy-vals"), FileFilter.include(TestSources.posLazyValsAllowlist)), compileFilesInDir("tests/pos-deep-subtype", allowDeepSubtypes), compileFilesInDir("tests/pos-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")), compileDir("tests/pos-special/java-param-names", defaultOptions.withJavacOnlyOptions("-parameters")), @@ -215,7 +217,9 @@ class CompilationTests { compileDir("tests/run-custom-args/Xmacro-settings/compileTimeEnv", defaultOptions.and("-Xmacro-settings:a,b=1,c.b.a=x.y.z=1,myLogger.level=INFO")), compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-language:experimental.captureChecking")), compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes), - compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init")) + compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init"), FileFilter.exclude("serialization-new.scala")), + // Run tests for experimental lightweight lazy vals and stable lazy vals. + compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init", "-Ylightweight-lazy-vals"), FileFilter.include(TestSources.runLazyValsAllowlist)), ).checkRuns() } diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index 410737c10c07..24d213df3a6f 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -43,24 +43,28 @@ object LazyVals { /* ------------- Start of public API ------------- */ + @experimental sealed trait LazyValControlState /** * Used to indicate the state of a lazy val that is being * evaluated and of which other threads await the result. */ + @experimental final class Waiting extends CountDownLatch(1) with LazyValControlState /** * Used to indicate the state of a lazy val that is currently being * evaluated with no other thread awaiting its result. */ + @experimental object Evaluating extends LazyValControlState /** * Used to indicate the state of a lazy val that has been evaluated to * `null`. */ + @experimental object NullValue extends LazyValControlState final val BITS_PER_LAZY_VAL = 2L @@ -80,6 +84,7 @@ object LazyVals { unsafe.compareAndSwapLong(t, offset, e, n) } + @experimental def objCAS(t: Object, offset: Long, exp: Object, n: Object): Boolean = { if (debug) println(s"objCAS($t, $exp, $n)") @@ -140,7 +145,9 @@ object LazyVals { r } + @experimental def getStaticFieldOffset(field: java.lang.reflect.Field): Long = { + @nowarn val r = unsafe.staticFieldOffset(field) if (debug) println(s"getStaticFieldOffset(${field.getDeclaringClass}, ${field.getName}) = $r") diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 927b54d86793..292630a91ed9 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -6,6 +6,9 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.MappedAlternative"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getStaticOffset"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.getOffsetStatic"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getStaticFieldOffset"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.getStaticFieldOffset"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.objCAS"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.evaluating"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.getStaticOffset"), @@ -14,11 +17,13 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waiting"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waitingAwaitRelease"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waitingRelease"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$LazyValControlState"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.controlState"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$Evaluating$"), - ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$NULL$"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$NullValue$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$Waiting"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.LazyVals.Evaluating"), - ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.LazyVals.NULL"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.LazyVals.NullValue"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.pureFunctions"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.captureChecking"), diff --git a/tests/printing/transformed/lazy-vals-legacy.flags b/tests/printing/transformed/lazy-vals-legacy.flags deleted file mode 100644 index a817c9c65fec..000000000000 --- a/tests/printing/transformed/lazy-vals-legacy.flags +++ /dev/null @@ -1 +0,0 @@ --Xlegacy-lazy-values \ No newline at end of file diff --git a/tests/printing/transformed/lazy-vals-new.check b/tests/printing/transformed/lazy-vals-new.check index 8c574cf8b895..b0cf2733032f 100644 --- a/tests/printing/transformed/lazy-vals-new.check +++ b/tests/printing/transformed/lazy-vals-new.check @@ -24,6 +24,14 @@ package { new scala.runtime.ModuleSerializationProxy(classOf[A]) @volatile lazy var x$lzy1: Object = null lazy def x(): Int = + { + var result: Object = A#x$lzy1 + if result.isInstanceOf[Int] then return scala.Int.unbox(result) else + if result.==(scala.runtime.LazyVals.NullValue) then + return scala.Int.unbox(null) + else return scala.Int.unbox(A.x$lzyINIT1()) + } + private def x$lzyINIT1(): Object = while do { val current: Object = A#x$lzy1 @@ -35,40 +43,50 @@ package { then { var result: Object = null + var resultNullable: Object = null try { - result = scala.Int.box(2) - if result.==(null) then + resultNullable = scala.Int.box(2) + if resultNullable.==(null) then result = scala.runtime.LazyVals.NullValue + else result = resultNullable + return resultNullable } finally - if - scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, - scala.runtime.LazyVals.Evaluating - , result).unary_!() - then - { - val lock: scala.runtime.LazyVals.LazyVals$Waiting = - A#x$lzy1.asInstanceOf[ - scala.runtime.LazyVals.LazyVals$Waiting - ] + { + if scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, - lock - , result) - lock.countDown() - } + scala.runtime.LazyVals.Evaluating + , result).unary_!() + then + { + val lock: scala.runtime.LazyVals.LazyVals$Waiting = + A#x$lzy1.asInstanceOf[ + scala.runtime.LazyVals.LazyVals$Waiting + ] + scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0 + , + lock, result) + lock.countDown() + } + else () + () + } } + else () else - if current.isInstanceOf[scala.runtime.LazyVals.Evaluating] then - { - scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, - scala.runtime.LazyVals.Evaluating - , new scala.runtime.LazyVals.LazyVals$Waiting()) - () - } - else - if current.isInstanceOf[scala.runtime.LazyVals.NullValue] then - return 0 + if + current.isInstanceOf[ + scala.runtime.LazyVals.LazyVals$LazyValControlState + ] + then + if current.==(scala.runtime.LazyVals.Evaluating) then + { + scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, + scala.runtime.LazyVals.Evaluating + , new scala.runtime.LazyVals.LazyVals$Waiting()) + () + } else if current.isInstanceOf[scala.runtime.LazyVals.LazyVals$Waiting] then @@ -76,8 +94,13 @@ package { current.asInstanceOf[scala.runtime.LazyVals.LazyVals$Waiting]. await () - else return scala.Int.unbox(current) + else + if current.==(scala.runtime.LazyVals.NullValue) then + return null + else () + else return current } } final lazy module val A: A = new A() -} \ No newline at end of file +} + diff --git a/tests/printing/transformed/lazy-vals-new.flags b/tests/printing/transformed/lazy-vals-new.flags new file mode 100644 index 000000000000..e6c43fc96531 --- /dev/null +++ b/tests/printing/transformed/lazy-vals-new.flags @@ -0,0 +1 @@ +-Ylightweight-lazy-vals \ No newline at end of file diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index d6c08f78ee85..8901ba53b605 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -67,6 +67,16 @@ val experimentalDefinitionInLibrary = Set( // Need experimental annotation macros to check that design works. "scala.quoted.Quotes.reflectModule.ClassDefModule.apply", "scala.quoted.Quotes.reflectModule.SymbolModule.newClass", + + // New APIs: Lightweight lazy vals. Can be stabilized in 3.3.0 + "scala.runtime.LazyVals$.Evaluating", + "scala.runtime.LazyVals$.Evaluating$", + "scala.runtime.LazyVals$.LazyValControlState", + "scala.runtime.LazyVals$.NullValue", + "scala.runtime.LazyVals$.NullValue$", + "scala.runtime.LazyVals$.Waiting", + "scala.runtime.LazyVals$.getStaticFieldOffset", + "scala.runtime.LazyVals$.objCAS" ) diff --git a/tests/run/lazyVals-legacy.check b/tests/run/lazyVals-legacy.check deleted file mode 100644 index aacc0b983ab8..000000000000 --- a/tests/run/lazyVals-legacy.check +++ /dev/null @@ -1,2 +0,0 @@ -42 -bar diff --git a/tests/run/lazyVals-legacy.flags b/tests/run/lazyVals-legacy.flags deleted file mode 100644 index a817c9c65fec..000000000000 --- a/tests/run/lazyVals-legacy.flags +++ /dev/null @@ -1 +0,0 @@ --Xlegacy-lazy-values \ No newline at end of file diff --git a/tests/run/lazyVals-legacy.scala b/tests/run/lazyVals-legacy.scala deleted file mode 100644 index f151ed5fe6ce..000000000000 --- a/tests/run/lazyVals-legacy.scala +++ /dev/null @@ -1,12 +0,0 @@ -object Test { - def foo = { - lazy val s = 42 - s - } - - lazy val bar = "bar" - - def main(args: Array[String]): Unit = - println(foo) - println(bar) -} diff --git a/tests/run/serialization-new-legacy.check b/tests/run/serialization-new-legacy.check new file mode 100644 index 000000000000..7124046aaff8 --- /dev/null +++ b/tests/run/serialization-new-legacy.check @@ -0,0 +1,226 @@ +a1 = Array[1,2,3] +_a1 = Array[1,2,3] +arrayEquals(a1, _a1): true + +e1 = Left(1) +_e1 = Left(1) +e1 eq _e1: false, _e1 eq e1: false +e1 equals _e1: true, _e1 equals e1: true + +x7 = RoundingMode +y7 = RoundingMode +x7 eq y7: true, y7 eq x7: true +x7 equals y7: true, y7 equals x7: true + +x8 = WeekDay +y8 = WeekDay +x8 eq y8: true, y8 eq x8: true +x8 equals y8: true, y8 equals x8: true + +x9 = UP +y9 = UP +x9 eq y9: true, y9 eq x9: true +x9 equals y9: true, y9 equals x9: true + +x10 = Monday +y10 = Monday +x10 eq y10: true, y10 eq x10: true +x10 equals y10: true, y10 equals x10: true + +x9 eq x10: false, x10 eq x9: false +x9 equals x10: false, x10 equals x9: false +x9 eq y10: false, y10 eq x9: false +x9 equals y10: false, y10 equals x9: false + +f1 = +_f1 = +f1(2): 4, _f1(2): 4 + +xs0 = List(1, 2, 3) +_xs0 = List(1, 2, 3) +xs0 eq _xs0: false, _xs0 eq xs0: false +xs0 equals _xs0: true, _xs0 equals xs0: true + +xs1 = List() +_xs1 = List() +xs1 eq _xs1: true, _xs1 eq xs1: true + +o1 = None +_o1 = None +o1 eq _o1: true, _o1 eq o1: true + +o2 = Some(1) +_o2 = Some(1) +o2 eq _o2: false, _o2 eq o2: false +o2 equals _o2: true, _o2 equals o2: true + +s1 = Symbol(hello) +_s1 = Symbol(hello) +s1 eq _s1: true, _s1 eq s1: true +s1 equals _s1: true, _s1 equals s1: true + +t1 = (BannerLimit,12345) +_t1 = (BannerLimit,12345) +t1 eq _t1: false, _t1 eq t1: false +t1 equals _t1: true, _t1 equals t1: true + +x = BitSet(1, 2) +y = BitSet(1, 2) +x equals y: true, y equals x: true + +x = BitSet(2, 3) +y = BitSet(2, 3) +x equals y: true, y equals x: true + +x = HashMap(1 -> A, 2 -> B, 3 -> C) +y = HashMap(1 -> A, 2 -> B, 3 -> C) +x equals y: true, y equals x: true + +x = HashSet(1, 2) +y = HashSet(1, 2) +x equals y: true, y equals x: true + +x = List((buffers,20), (layers,2), (title,3)) +y = List((buffers,20), (layers,2), (title,3)) +x equals y: true, y equals x: true + +x = ListMap(buffers -> 20, layers -> 2, title -> 3) +y = ListMap(buffers -> 20, layers -> 2, title -> 3) +x equals y: true, y equals x: true + +x = ListSet(3, 5) +y = ListSet(3, 5) +x equals y: true, y equals x: true + +x = Queue(a, b, c) +y = Queue(a, b, c) +x equals y: true, y equals x: true + +x = Range 0 until 10 +y = Range 0 until 10 +x equals y: true, y equals x: true + +x = NumericRange 0 until 10 +y = NumericRange 0 until 10 +x equals y: true, y equals x: true + +x = TreeMap(1 -> A, 2 -> B, 3 -> C) +y = TreeMap(1 -> A, 2 -> B, 3 -> C) +x equals y: true, y equals x: true + +x = TreeSet(1, 2, 3) +y = TreeSet(1, 2, 3) +x equals y: true, y equals x: true + +x = LazyList() +y = LazyList() +x equals y: true, y equals x: true + +x = TreeMap(42 -> FortyTwo) +y = TreeMap(42 -> FortyTwo) +x equals y: true, y equals x: true + +x = TreeSet(0, 2) +y = TreeSet(0, 2) +x equals y: true, y equals x: true + +x = Vector(Symbol(a), Symbol(b), Symbol(c)) +y = Vector(Symbol(a), Symbol(b), Symbol(c)) +x equals y: true, y equals x: true + +x = ArrayBuffer(one, two) +y = ArrayBuffer(one, two) +x equals y: true, y equals x: true + +x = ArrayBuilder.ofLong +y = ArrayBuilder.ofLong +x equals y: true, y equals x: true + +x = ArrayBuilder.ofFloat +y = ArrayBuilder.ofFloat +x equals y: true, y equals x: true + +x = BitSet(0, 8, 9) +y = BitSet(0, 8, 9) +x equals y: true, y equals x: true + +x = HashMap(A -> 1, B -> 2, C -> 3) +y = HashMap(A -> 1, B -> 2, C -> 3) +x equals y: true, y equals x: true + +x = HashSet(buffers, layers, title) +y = HashSet(buffers, layers, title) +x equals y: true, y equals x: true + +x = LinkedHashMap(Linked -> 1, Hash -> 2, Map -> 3) +y = LinkedHashMap(Linked -> 1, Hash -> 2, Map -> 3) +x equals y: true, y equals x: true + +x = List((Linked,1), (Hash,2), (Map,3)) +y = List((Linked,1), (Hash,2), (Map,3)) +x equals y: true, y equals x: true + +x = List((Linked,1), (Hash,2), (Map,3)) +y = List((Linked,1), (Hash,2), (Map,3)) +x equals y: true, y equals x: true + +x = LinkedHashSet(layers, buffers, title) +y = LinkedHashSet(layers, buffers, title) +x equals y: true, y equals x: true + +x = List(layers, buffers, title) +y = List(layers, buffers, title) +x equals y: true, y equals x: true + +x = List(layers, buffers, title) +y = List(layers, buffers, title) +x equals y: true, y equals x: true + +x = ListBuffer(white, black) +y = ListBuffer(white, black) +x equals y: true, y equals x: true + +x = Queue(20, 2, 3) +y = Queue(20, 2, 3) +x equals y: true, y equals x: true + +x = Stack(3, 2, 20) +y = Stack(3, 2, 20) +x equals y: true, y equals x: true + +x = abc +y = abc +x equals y: true, y equals x: true + +x = ArraySeq(1, 2, 3) +y = ArraySeq(1, 2, 3) +x equals y: true, y equals x: true + +x = TreeSet(1, 2, 3) +y = TreeSet(1, 2, 3) +x equals y: true, y equals x: true + +x = TrieMap(1 -> one, 2 -> two, 3 -> three) +y = TrieMap(1 -> one, 2 -> two, 3 -> three) +x equals y: true, y equals x: true + +x = Tim +y = Tim +x equals y: true, y equals x: true + +x = Bob +y = Bob +x equals y: true, y equals x: true + +x = John +y = John +x equals y: true, y equals x: true + +x = Bill +y = Bill +x equals y: true, y equals x: true + +x = Paul +y = Paul +x equals y: true, y equals x: true + diff --git a/tests/run/serialization-new-legacy.scala b/tests/run/serialization-new-legacy.scala new file mode 100644 index 000000000000..ac7c52a31797 --- /dev/null +++ b/tests/run/serialization-new-legacy.scala @@ -0,0 +1,483 @@ +// scalajs: --skip + +//############################################################################ +// Serialization +//############################################################################ + +object Serialize { + @throws(classOf[java.io.IOException]) + def write[A](o: A): Array[Byte] = { + val ba = new java.io.ByteArrayOutputStream(512) + val out = new java.io.ObjectOutputStream(ba) + out.writeObject(o) + out.close() + ba.toByteArray() + } + @throws(classOf[java.io.IOException]) + @throws(classOf[ClassNotFoundException]) + def read[A](buffer: Array[Byte]): A = { + val in = + new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(buffer)) + in.readObject().asInstanceOf[A] + } + def check[A, B](x: A, y: B): Unit = { + println("x = " + x) + println("y = " + y) + println("x equals y: " + (x equals y) + ", y equals x: " + (y equals x)) + assert((x equals y) && (y equals x)) + println() + } +} +import Serialize.* + +//############################################################################ +// Test classes in package "scala" + +object Test1_scala { + + private def arrayToString[A](arr: Array[A]): String = + arr.mkString("Array[",",","]") + + private def arrayEquals[A, B](a1: Array[A], a2: Array[B]): Boolean = + (a1.length == a2.length) && + (Iterator.range(0, a1.length) forall { i => a1(i) == a2(i) }) + + object WeekDay extends Enumeration { + type WeekDay = Value + val Monday, Tuesday, Wednesday, Thusday, Friday, Saturday, Sunday = Value + } + import WeekDay._, BigDecimal._, RoundingMode.* + + // in alphabetic order + try { + // Array + val a1 = Array(1, 2, 3) + val _a1: Array[Int] = read(write(a1)) + println("a1 = " + arrayToString(a1)) + println("_a1 = " + arrayToString(_a1)) + println("arrayEquals(a1, _a1): " + arrayEquals(a1, _a1)) + println() + + // Either + val e1 = Left(1) + val _e1: Either[Int, String] = read(write(e1)) + println("e1 = " + e1) + println("_e1 = " + _e1) + println("e1 eq _e1: " + (e1 eq _e1) + ", _e1 eq e1: " + (_e1 eq e1)) + println("e1 equals _e1: " + (e1 equals _e1) + ", _e1 equals e1: " + (_e1 equals e1)) + println() + + // Enumeration + val x7 = BigDecimal.RoundingMode + val y7: RoundingMode.type = read(write(x7)) + println("x7 = " + x7) + println("y7 = " + y7) + println("x7 eq y7: " + (x7 eq y7) + ", y7 eq x7: " + (y7 eq x7)) + println("x7 equals y7: " + (x7 equals y7) + ", y7 equals x7: " + (y7 equals x7)) + println() + + val x8 = WeekDay + val y8: WeekDay.type = read(write(x8)) + println("x8 = " + x8) + println("y8 = " + y8) + println("x8 eq y8: " + (x8 eq y8) + ", y8 eq x8: " + (y8 eq x8)) + println("x8 equals y8: " + (x8 equals y8) + ", y8 equals x8: " + (y8 equals x8)) + println() + + val x9 = UP + val y9: RoundingMode = read(write(x9)) + println("x9 = " + x9) + println("y9 = " + y9) + println("x9 eq y9: " + (x9 eq y9) + ", y9 eq x9: " + (y9 eq x9)) + println("x9 equals y9: " + (x9 equals y9) + ", y9 equals x9: " + (y9 equals x9)) + println() + + val x10 = Monday + val y10: WeekDay = read(write(x10)) + println("x10 = " + x10) + println("y10 = " + y10) + println("x10 eq y10: " + (x10 eq y10) + ", y10 eq x10: " + (y10 eq x10)) + println("x10 equals y10: " + (x10 equals y10) + ", y10 equals x10: " + (y10 equals x10)) + println() + + println("x9 eq x10: " + (x9 eq x10) + ", x10 eq x9: " + (x10 eq x9)) + println("x9 equals x10: " + (x9 equals x10) + ", x10 equals x9: " + (x10 equals x9)) + println("x9 eq y10: " + (x9 eq y10) + ", y10 eq x9: " + (y10 eq x9)) + println("x9 equals y10: " + (x9 equals y10) + ", y10 equals x9: " + (y10 equals x9)) + println() + + // Function + val f1 = { (x: Int) => 2 * x } + val _f1: Function[Int, Int] = read(write(f1)) + println("f1 = ") + println("_f1 = ") + println("f1(2): " + f1(2) + ", _f1(2): " + _f1(2)) + println() + + // List + val xs0 = List(1, 2, 3) + val _xs0: List[Int] = read(write(xs0)) + println("xs0 = " + xs0) + println("_xs0 = " + _xs0) + println("xs0 eq _xs0: " + (xs0 eq _xs0) + ", _xs0 eq xs0: " + (_xs0 eq xs0)) + println("xs0 equals _xs0: " + (xs0 equals _xs0) + ", _xs0 equals xs0: " + (_xs0 equals xs0)) + println() + + val xs1 = Nil + val _xs1: List[Nothing] = read(write(xs1)) + println("xs1 = " + xs1) + println("_xs1 = " + _xs1) + println("xs1 eq _xs1: " + (xs1 eq _xs1) + ", _xs1 eq xs1: " + (_xs1 eq xs1)) + println() + + // Option + val o1 = None + val _o1: Option[Nothing] = read(write(o1)) + println("o1 = " + o1) + println("_o1 = " + _o1) + println("o1 eq _o1: " + (o1 eq _o1) + ", _o1 eq o1: " + (_o1 eq o1)) + println() + + val o2 = Some(1) + val _o2: Option[Int] = read(write(o2)) + println("o2 = " + o2) + println("_o2 = " + _o2) + println("o2 eq _o2: " + (o2 eq _o2) + ", _o2 eq o2: " + (_o2 eq o2)) + println("o2 equals _o2: " + (o2 equals _o2) + ", _o2 equals o2: " + (_o2 equals o2)) + println() +/* + // Responder + val r1 = Responder.constant("xyz") + val _r1: Responder[String] = read(write(r1)) + check(r1, _r1) +*/ + // Symbol + val s1 = Symbol("hello") + val _s1: Symbol = read(write(s1)) + println("s1 = " + s1) + println("_s1 = " + _s1) + println("s1 eq _s1: " + (s1 eq _s1) + ", _s1 eq s1: " + (_s1 eq s1)) + println("s1 equals _s1: " + (s1 equals _s1) + ", _s1 equals s1: " + (_s1 equals s1)) + println() + + // Tuple + val t1 = ("BannerLimit", 12345) + val _t1: (String, Int) = read(write(t1)) + println("t1 = " + t1) + println("_t1 = " + _t1) + println("t1 eq _t1: " + (t1 eq _t1) + ", _t1 eq t1: " + (_t1 eq t1)) + println("t1 equals _t1: " + (t1 equals _t1) + ", _t1 equals t1: " + (_t1 equals t1)) + println() + } + catch { + case e: Exception => + println("Error in Test1_scala: " + e) + throw e + } +} + +//############################################################################ +// Test classes in package "scala.collection.immutable" + +object Test2_immutable { + import scala.collection.immutable.{ + BitSet, HashMap, HashSet, ListMap, ListSet, Queue, Range, SortedMap, + SortedSet, LazyList, TreeMap, TreeSet, Vector} + + // in alphabetic order + try { + // BitSet + val bs1 = BitSet.empty + 1 + 2 + val _bs1: BitSet = read(write(bs1)) + check(bs1, _bs1) + + val bs2 = { + val bs = new collection.mutable.BitSet() + bs += 2; bs += 3 + bs.toImmutable + } + val _bs2: BitSet = read(write(bs2)) + check(bs2, _bs2) + + // HashMap + val hm1 = HashMap.empty[Int, String] ++ List(1 -> "A", 2 -> "B", 3 -> "C") + val _hm1: HashMap[Int, String] = read(write(hm1)) + check(hm1, _hm1) + + // HashSet + val hs1 = HashSet.empty[Int] + 1 + 2 + val _hs1: HashSet[Int] = read(write(hs1)) + check(hs1, _hs1) + + // List + val xs1 = List(("buffers", 20), ("layers", 2), ("title", 3)) + val _xs1: List[(String, Int)] = read(write(xs1)) + check(xs1, _xs1) + + // ListMap + val lm1 = new ListMap[String, Int] ++ List("buffers" -> 20, "layers" -> 2, "title" -> 3) + val _lm1: ListMap[String, Int] = read(write(lm1)) + check(lm1, _lm1) + + // ListSet + val ls1 = new ListSet[Int] + 3 + 5 + val _ls1: ListSet[Int] = read(write(ls1)) + check(ls1, _ls1) + + // Queue + val q1 = Queue("a", "b", "c") + val _q1: Queue[String] = read(write(q1)) + check(q1, _q1) + + // Range + val r1 = 0 until 10 + val _r1: Range = read(write(r1)) + check(r1, _r1) + + val r2 = Range.Long(0L, 10L, 1) + val _r2: r2.type = read(write(r2)) + check(r2, _r2) + + // SortedMap + val sm1 = SortedMap.empty[Int, String] ++ List(2 -> "B", 3 -> "C", 1 -> "A") + val _sm1: SortedMap[Int, String] = read(write(sm1)) + check(sm1, _sm1) + + // SortedSet + val ss1 = SortedSet.empty[Int] + 2 + 3 + 1 + val _ss1: SortedSet[Int] = read(write(ss1)) + check(ss1, _ss1) + + // LazyList + val st1 = LazyList.range(0, 10) + val _st1: LazyList[Int] = read(write(st1)) + check(st1, _st1) + + // TreeMap + val tm1 = new TreeMap[Int, String] + (42 -> "FortyTwo") + val _tm1: TreeMap[Int, String] = read(write(tm1)) + check(tm1, _tm1) + + // TreeSet + val ts1 = new TreeSet[Int]() + 2 + 0 + val _ts1: TreeSet[Int] = read(write(ts1)) + check(ts1, _ts1) + + // Vector + val v1 = Vector(Symbol("a"), Symbol("b"), Symbol("c")) + val _v1: Vector[Symbol] = read(write(v1)) + check(v1, _v1) + } + catch { + case e: Exception => + println("Error in Test2_immutable: " + e) + throw e + } +} + +//############################################################################ +// Test classes in package "scala.collection.mutable" + +object Test3_mutable { + import scala.reflect.ClassTag + import scala.collection.mutable.{ + ArrayBuffer, ArrayBuilder, BitSet, + HashMap, HashSet, LinkedHashMap, LinkedHashSet, ListBuffer, + Queue, Stack, StringBuilder, ArraySeq, TreeSet} + import scala.collection.concurrent.TrieMap + + // in alphabetic order + try { + // ArrayBuffer + val ab1 = new ArrayBuffer[String] + ab1 ++= List("one", "two") + val _ab1: ArrayBuffer[String] = read(write(ab1)) + check(ab1, _ab1) + + // ArrayBuilder + val abu1 = ArrayBuilder.make[Long] + val _abu1: ArrayBuilder[ClassTag[Long]] = read(write(abu1)) + check(abu1, _abu1) + + val abu2 = ArrayBuilder.make[Float] + val _abu2: ArrayBuilder[ClassTag[Float]] = read(write(abu2)) + check(abu2, _abu2) + + // BitSet + val bs1 = new BitSet() + bs1 += 0 + bs1 += 8 + bs1 += 9 + val _bs1: BitSet = read(write(bs1)) + check(bs1, _bs1) + + // HashMap + val hm1 = new HashMap[String, Int] + hm1 ++= List(("A", 1), ("B", 2), ("C", 3)).iterator + val _hm1: HashMap[String, Int] = read(write(hm1)) + check(hm1, _hm1) + + // HashSet + val hs1 = new HashSet[String] + hs1 ++= List("layers", "buffers", "title").iterator + val _hs1: HashSet[String] = read(write(hs1)) + check(hs1, _hs1) + + // LinkedHashMap + { val lhm1 = new LinkedHashMap[String, Int] + val list = List(("Linked", 1), ("Hash", 2), ("Map", 3)) + lhm1 ++= list.iterator + val _lhm1: LinkedHashMap[String, Int] = read(write(lhm1)) + check(lhm1, _lhm1) + check(lhm1.toSeq, _lhm1.toSeq) // check elements order + check(lhm1.toSeq, list) // check elements order + } + + // LinkedHashSet + { val lhs1 = new LinkedHashSet[String] + val list = List("layers", "buffers", "title") + lhs1 ++= list.iterator + val _lhs1: LinkedHashSet[String] = read(write(lhs1)) + check(lhs1, _lhs1) + check(lhs1.toSeq, _lhs1.toSeq) // check elements order + check(lhs1.toSeq, list) // check elements order + } + + // ListBuffer + val lb1 = new ListBuffer[String] + lb1 ++= List("white", "black") + val _lb1: ListBuffer[String] = read(write(lb1)) + check(lb1, _lb1) + + // Queue + val q1 = new Queue[Int] + q1 ++= List(20, 2, 3).iterator + val _q1: Queue[Int] = read(write(q1)) + check(q1, _q1) + + // Stack + val s1 = new Stack[Int] + s1 pushAll q1 + val _s1: Stack[Int] = read(write(s1)) + check(s1, _s1) + + // StringBuilder + val sb1 = new StringBuilder + sb1 append "abc" + val _sb1: StringBuilder = read(write(sb1)) + check(sb1, _sb1) + + // ArraySeq + val wa1 = ArraySeq.make(Array(1, 2, 3)) + val _wa1: ArraySeq[Int] = read(write(wa1)) + check(wa1, _wa1) + + // TreeSet + val ts1 = TreeSet[Int]() ++= Array(1, 2, 3) + val _ts1: TreeSet[Int] = read(write(ts1)) + check(ts1, _ts1) + + // concurrent.TrieMap + val ct1 = TrieMap[Int, String]() ++= Array(1 -> "one", 2 -> "two", 3 -> "three") + val _ct1: TrieMap[Int, String] = read(write(ct1)) + check(ct1, _ct1) + } + catch { + case e: Exception => + println("Error in Test3_mutable: " + e) + throw e + } +} + +//############################################################################ +// Test user-defined classes WITHOUT nesting + +class Person(_name: String) extends Serializable { + private var name = _name + override def toString() = name + override def equals(that: Any): Boolean = + that.isInstanceOf[Person] && + (name == that.asInstanceOf[Person].name) +} + +class Employee(_name: String) extends Serializable { + private var name = _name + override def toString() = name +} + +object bob extends Employee("Bob") + +object Test5 { + val x1 = new Person("Tim") + val x2 = bob + + try { + val y1: Person = read(write(x1)) + val y2: Employee = read(write(x2)) + + check(x1, y1) + check(x2, y2) + } + catch { + case e: Exception => + println("Error in Test5: " + e) + } +} + +//############################################################################ +// Test user-defined classes WITH nesting + +object Test6 { + object bill extends Employee("Bill") { + val x = paul + } + object paul extends Person("Paul") { + val x = 4 // bill; => StackOverflowException !!! + } + val x1 = new Person("John") + val x2 = bill + val x3 = paul + + try { + val y1: Person = read(write(x1)) + val y2: Employee = read(write(x2)) + val y3: Person = read(write(x3)) + + check(x1, y1) + check(x2, y2) + check(x3, y3) + } + catch { + case e: Exception => + println("Error in Test6: " + e) + } +} + +//############################################################################ +// Nested objects cannot get readresolve automatically because after deserialization +// they would be null (they are treated as lazy vals) +class Outer extends Serializable { + object Inner extends Serializable +} + +object Test7 { + val x = new Outer + x.Inner // initialize + val y:Outer = read(write(x)) + if (y.Inner == null) + println("Inner object is null") +} + +//############################################################################ +// Test code + +object Test { + def main(args: Array[String]): Unit = { + Test1_scala + Test2_immutable + Test3_mutable + Test5 + Test6 + Test7 + } +} From 572c6742d2eb9f1bf8ff62bbd05c317b9fc7a2b0 Mon Sep 17 00:00:00 2001 From: Szymon Rodziewicz Date: Tue, 4 Oct 2022 17:12:25 +0200 Subject: [PATCH 6/7] Revert scala-parallel-collection community build to main branch --- community-build/community-projects/scala-parallel-collections | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community-build/community-projects/scala-parallel-collections b/community-build/community-projects/scala-parallel-collections index 1cd213661a68..a6bd648bb188 160000 --- a/community-build/community-projects/scala-parallel-collections +++ b/community-build/community-projects/scala-parallel-collections @@ -1 +1 @@ -Subproject commit 1cd213661a682e7c9947a1d1777526f01225da56 +Subproject commit a6bd648bb188a65ab36be07e956e52fe25f64d67 From f62ffd8eecc5ed5c277b8f5e4171f44c2d9b225e Mon Sep 17 00:00:00 2001 From: Szymon Rodziewicz Date: Mon, 10 Oct 2022 20:06:26 +0200 Subject: [PATCH 7/7] Clean-up the new lazy vals --- bench-micro/results_isStable.json | 0 .../dotty/tools/dotc/core/Definitions.scala | 6 + .../dotty/tools/dotc/transform/LazyVals.scala | 249 +++++++----------- .../tools/dotc/transform/TreeChecker.scala | 1 + .../test/dotc/pos-lazy-vals-tests.allowlist | 2 +- compiler/test/dotc/run-from-tasty.blacklist | 2 +- .../test/dotc/run-lazy-vals-tests.allowlist | 2 +- .../test/dotc/run-test-pickling.blacklist | 2 +- compiler/test/dotc/run-test-recheck.excludes | 2 +- library/src/scala/runtime/LazyVals.scala | 9 - project/MiMaFilters.scala | 11 - .../transformed/lazy-vals-legacy.scala | 2 +- .../printing/transformed/lazy-vals-new.check | 61 ++--- .../printing/transformed/lazy-vals-new.flags | 2 +- .../printing/transformed/lazy-vals-new.scala | 2 +- tests/run/lazyVals_c3.1.0.scala | 2 +- 16 files changed, 140 insertions(+), 215 deletions(-) delete mode 100644 bench-micro/results_isStable.json diff --git a/bench-micro/results_isStable.json b/bench-micro/results_isStable.json deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b88e7c88d449..174244b4a456 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1990,6 +1990,12 @@ class Definitions { addSyntheticSymbolsComments } + /** Definitions used in Lazy Vals implementation */ + val LazyValsModuleName = "scala.runtime.LazyVals" + @tu lazy val LazyValsModule = requiredModule(LazyValsModuleName) + @tu lazy val LazyValsWaitingState = requiredClass(s"$LazyValsModuleName.Waiting") + @tu lazy val LazyValsControlState = requiredClass(s"$LazyValsModuleName.LazyValControlState") + def addSyntheticSymbolsComments(using Context): Unit = def add(sym: Symbol, doc: String) = ctx.docCtx.foreach(_.addDocstring(sym, Some(Comment(NoSpan, doc)))) diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index d8f4cbffb623..3b37ef130231 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -27,14 +27,9 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { /** * The map contains the list of the offset trees. */ - class OffsetInfo(var defs: List[Tree]) - /** - * This map contains mutable state of transformation: OffsetDefs to be appended - * to companion object definitions, and number of bits currently used. - */ - class LegacyOffsetInfo(defs: List[Tree], var ord: Int) extends OffsetInfo(defs) + class OffsetInfo(var defs: List[Tree], var ord: Int = 0) + private val appendOffsetDefs = mutable.Map.empty[Symbol, OffsetInfo] - private val oldAppendOffsetDefs = mutable.Map.empty[Symbol, LegacyOffsetInfo] override def phaseName: String = LazyVals.name @@ -60,11 +55,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { else nullables.toList } - private def needsBoxing(tp: Type)(using Context): Boolean = tp != NoType && tp != defn.UnitType && tp.classSymbol.isPrimitiveValueClass - - private def boxIfCan(tp: Type)(using Context): Type = - assert(needsBoxing(tp)) - defn.boxedType(tp) + private def needsBoxing(tp: Type)(using Context): Boolean = tp.classSymbol.isPrimitiveValueClass override def prepareForUnit(tree: Tree)(using Context): Context = { if (lazyValNullables == null) @@ -113,12 +104,12 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } } - /** - * Append offset fields to companion objects. + + /** Append offset fields to companion objects */ override def transformTemplate(template: Template)(using Context): Tree = { val cls = ctx.owner.asClass - (if !ctx.settings.YlightweightLazyVals.value then oldAppendOffsetDefs else appendOffsetDefs).get(cls) match { + appendOffsetDefs.get(cls) match { case None => template case Some(data) => data.defs.foreach(_.symbol.addAnnotation(Annotation(defn.ScalaStaticAnnot))) @@ -131,10 +122,10 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { case _ => prefix ::: stats } - /** - * Make an eager val that would implement synthetic module. - * Eager val ensures thread safety and has less code generated. - */ + /** Make an eager val that would implement synthetic module. + * Eager val ensures thread safety and has less code generated. + * + */ def transformSyntheticModule(tree: ValOrDefDef)(using Context): Thicket = { val sym = tree.symbol val holderSymbol = newSymbol(sym.owner, LazyLocalName.fresh(sym.asTerm.name), @@ -144,8 +135,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { Thicket(field, getter) } - /** - * Desugar a local `lazy val x: Int = ` into: + /** Desugar a local `lazy val x: Int = ` into: * * ``` * val x$lzy = new scala.runtime.LazyInt() @@ -285,132 +275,99 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } /** - * Create a threadsafe lazy accessor equivalent to the following code: + * Create a threadsafe lazy accessor and function that computes the field's value. `Evaluating` and + * `NullValue` are represented by `object`s and `Waiting` by a class that allows awaiting the completion + * of the evaluation. Note that since tail-recursive functions are transformed *before* lazy-vals, + * this implementation does involve explicit while loop. `PatternMatcher` is coming before `LazyVals`, + * therefore the pattern matching is implemented using if-s. + * * ``` * private @volatile var _x: AnyRef = null - * @tailrec def x: A = - * _x match - * case current: A => - * current - * case NullValue => null - * case null => - * if CAS(_x, null, Evaluating) then - * var result: AnyRef = null // here, we need `AnyRef` to possibly assign `NullValue` - * try - * result = rhs - * nullable = null // if the field is nullable; see `CollectNullableFields` - * finally - * if result == null then result = NullValue // drop if A is non-nullable - * if !CAS(_x, Evaluating, result) then - * val lock = _x.asInstanceOf[Waiting] - * CAS(_x, lock, result) - * lock.release() - * x - * case Evaluating => - * CAS(_x, Evaluating, new Waiting) - * x - * case current: Waiting => - * current.awaitRelease() - * x - * ``` - * Where `Evaluating` and `NullValue` are represented by `object`s and `Waiting` by a class that - * allows awaiting the completion of the evaluation. Note that since tail-recursive - * functions are transformed *before* lazy-vals, this implementation directly implements - * the resulting loop. `PatternMatcher` coming before `LazyVals`, the pattern matching block - * is implemented using if-s. Additionally, the code can be optimized to be better inlined by - * introducing separate function for computing the lazy value and awaiting it. That is: - * - * ``` - * private var _x: AnyRef = null * * def x: A = - * if !(_x == null || _x.isInstanceOf[LazyValControlState]) then - * return _x.asInstanceOf[A] - * else if _x == NullValue then - * return null - * else - * return x_compute() + * val result = _x + * if result.isInstanceOf[A] then + * result // possible unboxing applied here + * else if result.eq(NullValue) then + * null // possible unboxing applied here + * else + * x_compute() // possible unboxing applied here * - * private def x_compute() = + * private def x_compute(): AnyRef = * while do * val current: AnyRef = _x - * if current == null then + * if current.eq(null) then * if CAS(_x, null, Evaluating) then * var resultNullable: AnyRef = null * var result: AnyRef = null * try * resultNullable = rhs - * nullable = null - * if resultNullable == null then + * nullable = null // nulls out the nullable fields used only in initialization + * if resultNullable.eq(null) then * result = NullValue * else * result = resultNullable - * return result * finally * if !CAS(_x, Evaluating, result) then * val lock = _x.asInstanceOf[Waiting] * CAS(_x, lock, result) * lock.release() + * return resultNullable * else * if current.isInstanceOf[LazyValControlState] then - * if current.isInstanceOf[Evaluating] then // To avoid creating Waiting instance - * CAS(current, Evaluating, new Waiting) - * else if current.isInstanceOf[NullValue] then - * return null + * if current.eq(Evaluating) then // To avoid creating Waiting instance + * CAS(current, current, new Waiting) * else if current.isInstanceOf[Waiting] then * current.asInstanceOf[Waiting].await() + * else return null * else - * return current.asInstanceOf[A] + * return current * end while * * ``` - * + * * @param memberDef the transformed lazy field member definition * @param claz the class containing this lazy val field * @param target the target synthetic field * @param offset the offset of the field in the storage allocation of the class * @param thiz a reference to the transformed class - * @param helperModule the symbol of the runtime LazyVals helper module - * @param runtimeModule the runtime LazyVals module path - */ def mkThreadSafeDef(memberDef: ValOrDefDef, claz: ClassSymbol, target: Symbol, offset: Tree, - thiz: Tree, - helperModule: TermSymbol, - runtimeModule: String)(using Context): (DefDef, DefDef) = { - val tp = memberDef.tpe.widen.resultType.widen - val waiting = ref(requiredClass(s"$runtimeModule.${lazyNme.RLazyVals.waiting}")) - val controlState = ref(requiredClass(s"$runtimeModule.${lazyNme.RLazyVals.controlState}")) - val evaluating = Select(ref(helperModule), lazyNme.RLazyVals.evaluating) - val nullValue = Select(ref(helperModule), lazyNme.RLazyVals.nullValue) - val objCasFlag = Select(ref(helperModule), lazyNme.RLazyVals.objCas) + thiz: Tree)(using Context): (DefDef, DefDef) = { + val tp = memberDef.tpe.widenDealias.resultType.widenDealias + val waiting = ref(defn.LazyValsWaitingState) + val controlState = ref(defn.LazyValsControlState) + val evaluating = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.evaluating) + val nullValue = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.nullValue) + val objCasFlag = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.objCas) val accessorMethodSymbol = memberDef.symbol.asTerm val lazyInitMethodName = LazyLocalInitName.fresh(memberDef.name.asTermName) val lazyInitMethodSymbol = newSymbol(claz, lazyInitMethodName, Synthetic | Method | Private, MethodType(Nil)(_ => Nil, _ => defn.ObjectType)) val rhs = memberDef.rhs val rhsMappedOwner = rhs.changeOwnerAfter(memberDef.symbol, lazyInitMethodSymbol, this) - val valueSymbol = newSymbol(accessorMethodSymbol, lazyNme.result, Synthetic | Mutable, defn.ObjectType) + val valueSymbol = newSymbol(accessorMethodSymbol, lazyNme.result, Synthetic, defn.ObjectType) val immediateValueCondition = - if (controlState.tpe <:< tp) then - ref(valueSymbol).select(defn.Any_!=).appliedTo(nullLiteral).select(nme.And).appliedTo(ref(valueSymbol).select(defn.Any_isInstanceOf).appliedToTypeTree(controlState) + if (defn.LazyValsControlState.isSubClass(tp.classSymbol)) then + ref(valueSymbol).select(defn.Any_!=).appliedTo(nullLiteral).select(nme.And).appliedTo(ref(valueSymbol) + .select(defn.Any_isInstanceOf).appliedToType(defn.LazyValsControlState.typeRef) .select(nme.UNARY_!).appliedToNone) else - ref(valueSymbol).select(defn.Any_isInstanceOf).appliedToTypeTree(TypeTree(tp)) + ref(valueSymbol).select(defn.Any_isInstanceOf).appliedToType(tp) val accessorBody = Block( ValDef(valueSymbol, ref(target)) :: Nil, If( // if _x != null && !_x.isInstanceOf[LazyValControlState] then immediateValueCondition, - Return(ref(valueSymbol).ensureConforms(tp), accessorMethodSymbol), // then return _x.asInstanceOf[A] + ref(valueSymbol).ensureConforms(tp), // then return _x.asInstanceOf[A] If( - ref(valueSymbol).select(defn.Any_==).appliedTo(nullValue), - Return(nullLiteral.ensureConforms(tp), accessorMethodSymbol), - Return(ref(lazyInitMethodSymbol).ensureApplied.ensureConforms(tp), accessorMethodSymbol) // else return x_compute() + ref(valueSymbol).select(defn.Object_eq).appliedTo(nullValue), + nullLiteral.ensureConforms(tp), + ref(lazyInitMethodSymbol).ensureApplied.ensureConforms(tp) // else return x_compute() ) ) ) @@ -430,31 +387,31 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { ref(lockSymb).select(lazyNme.RLazyVals.waitingRelease).ensureApplied) } // finally block - val fin = Block(If( + val fin = If( objCasFlag.appliedTo(thiz, offset, evaluating, ref(resSymb)).select(nme.UNARY_!).appliedToNone, lockRel, unitLiteral - ) :: Nil, unitLiteral).withType(defn.UnitType) + ) // entire try block val evaluate = Try( Block( - (Assign(ref(resSymbNullable), if needsBoxing(tp) && rhsMappedOwner != EmptyTree then rhsMappedOwner.ensureConforms(boxIfCan(tp)) else rhsMappedOwner) // try result = rhs + (Assign(ref(resSymbNullable), if needsBoxing(tp) && rhsMappedOwner != EmptyTree then rhsMappedOwner.ensureConforms(defn.boxedType(tp)) else rhsMappedOwner) // try result = rhs :: If( - ref(resSymbNullable).select(defn.Any_==).appliedTo(nullLiteral), + ref(resSymbNullable).select(defn.Object_eq).appliedTo(nullLiteral), Assign(ref(resSymb), nullValue), Assign(ref(resSymb), ref(resSymbNullable)) ) :: Nil) ::: nullOut(nullableFor(accessorMethodSymbol)), - Return(ref(resSymbNullable), lazyInitMethodSymbol)), + unitLiteral), Nil, fin ) // if CAS(_, null, Evaluating) If( objCasFlag.appliedTo(thiz, offset, nullLiteral, evaluating), - Block(ValDef(resSymb, nullLiteral) :: ValDef(resSymbNullable, nullLiteral) :: Nil, // var result: AnyRef = null - evaluate), // try ... finally ... + Block(ValDef(resSymb, nullLiteral) :: ValDef(resSymbNullable, nullLiteral) :: evaluate :: Nil, // var result: AnyRef = null + Return(ref(resSymbNullable), lazyInitMethodSymbol)), unitLiteral ).withType(defn.UnitType) } @@ -465,10 +422,10 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(controlState), // if a control state If( - ref(current).select(defn.Any_==).appliedTo(evaluating), + ref(current).select(defn.Object_eq).appliedTo(evaluating), // if is Evaluating then CAS(_, Evaluating, new Waiting) Block( - objCasFlag.appliedTo(thiz, offset, evaluating, Select(New(waiting), StdNames.nme.CONSTRUCTOR).ensureApplied) :: Nil, + objCasFlag.appliedTo(thiz, offset, ref(current), Select(New(waiting), StdNames.nme.CONSTRUCTOR).ensureApplied) :: Nil, unitLiteral ), // if not Evaluating @@ -476,17 +433,14 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { ref(current).select(defn.Any_isInstanceOf).appliedToTypeTree(waiting), // if is waiting ref(current).select(defn.Any_asInstanceOf).appliedToTypeTree(waiting).select(lazyNme.RLazyVals.waitingAwaitRelease, _.info.paramInfoss.exists(_.size == 0)).ensureApplied, - If(ref(current).select(defn.Any_==).appliedTo(nullValue), - Return(nullLiteral, lazyInitMethodSymbol), - unitLiteral - ) + Return(nullLiteral, lazyInitMethodSymbol) ) ), // if not a control state Return(ref(current), lazyInitMethodSymbol) ) - val initBody = Block(ValDef(current, ref(target)) :: Nil, If(ref(current).equal(nullLiteral), initialize, ifNotUninitialized).withType(defn.UnitType)) + val initBody = Block(ValDef(current, ref(target)) :: Nil, If(ref(current).select(defn.Object_eq).appliedTo(nullLiteral), initialize, ifNotUninitialized).withType(defn.UnitType)) val initMainLoop = WhileDo(EmptyTree, initBody) // becomes: while (true) do { body } val initMethodDef = DefDef(lazyInitMethodSymbol, initMainLoop) (accessorDef, initMethodDef) @@ -504,15 +458,12 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { import dotty.tools.dotc.core.Types._ import dotty.tools.dotc.core.Flags._ - val runtimeModule = "scala.runtime.LazyVals" val claz = x.symbol.owner.asClass val thizClass = Literal(Constant(claz.info)) - val helperModule = requiredModule(runtimeModule) - var offsetSymbol: TermSymbol | Null = null def offsetName(id: Int) = s"${StdNames.nme.LAZY_FIELD_OFFSET}${if (x.symbol.owner.is(Module)) "_m_" else ""}$id".toTermName val containerName = LazyLocalName.fresh(x.name.asTermName) - val containerSymbol = newSymbol(claz, containerName, containerFlags, defn.ObjectType).enteredAfter(this) + val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags | Private, defn.ObjectType, coord = x.symbol.coord).enteredAfter(this) containerSymbol.addAnnotation(Annotation(defn.VolatileAnnot)) // private @volatile var _x: AnyRef containerSymbol.addAnnotations(x.symbol.annotations) // pass annotations from original definition val stat = x.symbol.isStatic @@ -520,26 +471,22 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { containerSymbol.setFlag(JavaStatic) val getOffset = if stat then - Select(ref(helperModule), lazyNme.RLazyVals.getStaticFieldOffset) + Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.getStaticFieldOffset) else - Select(ref(helperModule), lazyNme.RLazyVals.getOffsetStatic) + Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.getOffsetStatic) val containerTree = ValDef(containerSymbol, nullLiteral) // create an offset for this lazy val - appendOffsetDefs.get(claz) match + val offsetSymbol: TermSymbol = appendOffsetDefs.get(claz) match case Some(info) => - offsetSymbol = newSymbol(claz, offsetName(info.defs.size), Synthetic, defn.LongType).enteredAfter(this) - offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot)) - val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(containerName.mangledString))) - val offsetTree = ValDef(offsetSymbol.nn, getOffset.appliedTo(fieldTree)) - info.defs = offsetTree :: info.defs + newSymbol(claz, offsetName(info.defs.size), Synthetic, defn.LongType).enteredAfter(this) case None => - offsetSymbol = newSymbol(claz, offsetName(0), Synthetic, defn.LongType).enteredAfter(this) - offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot)) - val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(containerName.mangledString))) - val offsetTree = ValDef(offsetSymbol.nn, getOffset.appliedTo(fieldTree)) - appendOffsetDefs += (claz -> new OffsetInfo(List(offsetTree))) - + newSymbol(claz, offsetName(0), Synthetic, defn.LongType).enteredAfter(this) + offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot)) + val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(containerName.mangledString))) + val offsetTree = ValDef(offsetSymbol.nn, getOffset.appliedTo(fieldTree)) + val offsetInfo = appendOffsetDefs.getOrElseUpdate(claz, new OffsetInfo(Nil)) + offsetInfo.defs = offsetTree :: offsetInfo.defs val offset = ref(offsetSymbol.nn) val swapOver = @@ -548,7 +495,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { else This(claz) - val (accessorDef, initMethodDef) = mkThreadSafeDef(x, claz, containerSymbol, offset, swapOver, helperModule, runtimeModule) + val (accessorDef, initMethodDef) = mkThreadSafeDef(x, claz, containerSymbol, offset, swapOver) Thicket(containerTree, accessorDef, initMethodDef) } @@ -656,9 +603,8 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { val tpe = x.tpe.widen.resultType.widen val claz = x.symbol.owner.asClass val thizClass = Literal(Constant(claz.info)) - val helperModule = requiredModule("scala.runtime.LazyVals") - val getOffset = Select(ref(helperModule), lazyNme.RLazyVals.getOffset) - val getOffsetStatic = Select(ref(helperModule), lazyNme.RLazyVals.getOffsetStatic) + val getOffset = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.getOffset) + val getOffsetStatic = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.getOffsetStatic) var offsetSymbol: TermSymbol | Null = null var flag: Tree = EmptyTree var ord = 0 @@ -666,7 +612,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def offsetName(id: Int) = s"${StdNames.nme.LAZY_FIELD_OFFSET}${if (x.symbol.owner.is(Module)) "_m_" else ""}$id".toTermName // compute or create appropriate offsetSymbol, bitmap and bits used by current ValDef - oldAppendOffsetDefs.get(claz) match { + appendOffsetDefs.get(claz) match { case Some(info) => val flagsPerLong = (64 / scala.runtime.LazyVals.BITS_PER_LAZY_VAL).toInt info.ord += 1 @@ -696,20 +642,19 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { flag = ValDef(flagSymbol, Literal(Constant(0L))) val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(flagName.toString))) val offsetTree = ValDef(offsetSymbol.nn, getOffsetStatic.appliedTo(fieldTree)) - oldAppendOffsetDefs += (claz -> new LegacyOffsetInfo(List(offsetTree), ord)) + appendOffsetDefs += (claz -> new OffsetInfo(List(offsetTree), ord)) } val containerName = LazyLocalName.fresh(x.name.asTermName) val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags, tpe, coord = x.symbol.coord).enteredAfter(this) - val containerTree = ValDef(containerSymbol, defaultValue(tpe)) val offset = ref(offsetSymbol.nn) - val getFlag = Select(ref(helperModule), lazyNme.RLazyVals.get) - val setFlag = Select(ref(helperModule), lazyNme.RLazyVals.setFlag) - val wait = Select(ref(helperModule), lazyNme.RLazyVals.wait4Notification) - val state = Select(ref(helperModule), lazyNme.RLazyVals.state) - val cas = Select(ref(helperModule), lazyNme.RLazyVals.cas) + val getFlag = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.get) + val setFlag = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.setFlag) + val wait = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.wait4Notification) + val state = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.state) + val cas = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.cas) val accessor = mkThreadSafeDefLegacy(x.symbol.asTerm, claz, ord, containerSymbol, x.rhs, tpe, offset, getFlag, state, cas, setFlag, wait) if (flag eq EmptyTree) @@ -725,22 +670,20 @@ object LazyVals { import Names.TermName object RLazyVals { import scala.runtime.LazyVals.{Names => N} - val waiting: TermName = N.waiting.toTermName - val waitingAwaitRelease: TermName = N.waitingAwaitRelease.toTermName - val waitingRelease: TermName = N.waitingRelease.toTermName - val evaluating: TermName = N.evaluating.toTermName - val controlState: TermName = N.controlState.toTermName - val nullValue: TermName = N.nullValue.toTermName - val objCas: TermName = N.objCas.toTermName - val getStaticFieldOffset: TermName = N.getStaticFieldOffset.toTermName - val get: TermName = N.get.toTermName - val setFlag: TermName = N.setFlag.toTermName - val wait4Notification: TermName = N.wait4Notification.toTermName - val state: TermName = N.state.toTermName - val cas: TermName = N.cas.toTermName - val getOffset: TermName = N.getOffset.toTermName - val getOffsetStatic: TermName = N.getOffsetStatic.toTermName - val getDeclaredField: TermName = "getDeclaredField".toTermName + val waitingAwaitRelease: TermName = "await".toTermName + val waitingRelease: TermName = "countDown".toTermName + val evaluating: TermName = "Evaluating".toTermName + val nullValue: TermName = "NullValue".toTermName + val objCas: TermName = "objCAS".toTermName + val get: TermName = N.get.toTermName + val setFlag: TermName = N.setFlag.toTermName + val wait4Notification: TermName = N.wait4Notification.toTermName + val state: TermName = N.state.toTermName + val cas: TermName = N.cas.toTermName + val getOffset: TermName = N.getOffset.toTermName + val getOffsetStatic: TermName = "getOffsetStatic".toTermName + val getStaticFieldOffset: TermName = "getStaticFieldOffset".toTermName + val getDeclaredField: TermName = "getDeclaredField".toTermName } val flag: TermName = "flag".toTermName val state: TermName = "state".toTermName diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index bbc62a8361e2..82413e2e6733 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -498,6 +498,7 @@ class TreeChecker extends Phase with SymTransformer { def isAllowed(sym: Symbol): Boolean = sym.is(ConstructorProxy) val symbolsNotDefined = (decls -- defined - constr.symbol).filterNot(isAllowed) + assert(symbolsNotDefined.isEmpty, i" $cls tree does not define members: ${symbolsNotDefined.toList}%, %\n" + i"expected: ${decls.toList}%, %\n" + diff --git a/compiler/test/dotc/pos-lazy-vals-tests.allowlist b/compiler/test/dotc/pos-lazy-vals-tests.allowlist index 227cc687b9db..21667a9265d7 100644 --- a/compiler/test/dotc/pos-lazy-vals-tests.allowlist +++ b/compiler/test/dotc/pos-lazy-vals-tests.allowlist @@ -34,4 +34,4 @@ t6278-synth-def.scala t6925b.scala t7011.scala t8306.scala -zipped.scala \ No newline at end of file +zipped.scala diff --git a/compiler/test/dotc/run-from-tasty.blacklist b/compiler/test/dotc/run-from-tasty.blacklist index b6266125761a..2c483e9e34b6 100644 --- a/compiler/test/dotc/run-from-tasty.blacklist +++ b/compiler/test/dotc/run-from-tasty.blacklist @@ -1,2 +1,2 @@ # CI only: cannot reduce summonFrom with -sip23-valueof.scala \ No newline at end of file +sip23-valueof.scala diff --git a/compiler/test/dotc/run-lazy-vals-tests.allowlist b/compiler/test/dotc/run-lazy-vals-tests.allowlist index 2edc6c1050d7..98973dc2893d 100644 --- a/compiler/test/dotc/run-lazy-vals-tests.allowlist +++ b/compiler/test/dotc/run-lazy-vals-tests.allowlist @@ -63,4 +63,4 @@ t7406.scala t8245.scala unapply.scala unit-lazy-val.scala -view-iterator-stream.scala \ No newline at end of file +view-iterator-stream.scala diff --git a/compiler/test/dotc/run-test-pickling.blacklist b/compiler/test/dotc/run-test-pickling.blacklist index bd74832cf6a6..9f19b439135c 100644 --- a/compiler/test/dotc/run-test-pickling.blacklist +++ b/compiler/test/dotc/run-test-pickling.blacklist @@ -43,4 +43,4 @@ i12753 t6138 t6138-2 i12656.scala -trait-static-forwarder \ No newline at end of file +trait-static-forwarder diff --git a/compiler/test/dotc/run-test-recheck.excludes b/compiler/test/dotc/run-test-recheck.excludes index d6a5d0f2d594..de2fb3f8aed8 100644 --- a/compiler/test/dotc/run-test-recheck.excludes +++ b/compiler/test/dotc/run-test-recheck.excludes @@ -10,4 +10,4 @@ i5976.scala tagless.scala safeThrowsStrawman2.scala t7584.scala -function-arity.scala \ No newline at end of file +function-arity.scala diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index 24d213df3a6f..0bb78aee94ad 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -163,20 +163,11 @@ object LazyVals { object Names { - final val controlState = "LazyValControlState" - final val waiting = "Waiting" - final val evaluating = "Evaluating" - final val nullValue = "NullValue" - final val waitingAwaitRelease = "await" - final val waitingRelease = "countDown" final val state = "STATE" final val cas = "CAS" - final val objCas = "objCAS" final val setFlag = "setFlag" final val wait4Notification = "wait4Notification" final val get = "get" final val getOffset = "getOffset" - final val getOffsetStatic = "getOffsetStatic" - final val getStaticFieldOffset = "getStaticFieldOffset" } } diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 292630a91ed9..81510d22d2c2 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -5,20 +5,9 @@ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.MappedAlternative"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getStaticOffset"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.getOffsetStatic"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getStaticFieldOffset"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.getStaticFieldOffset"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.objCAS"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.evaluating"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.getStaticOffset"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.nullValue"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.objCas"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waiting"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waitingAwaitRelease"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.waitingRelease"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$LazyValControlState"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals#Names.controlState"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$Evaluating$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$NullValue$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.LazyVals$Waiting"), diff --git a/tests/printing/transformed/lazy-vals-legacy.scala b/tests/printing/transformed/lazy-vals-legacy.scala index 28387acec9c5..7240286af56a 100644 --- a/tests/printing/transformed/lazy-vals-legacy.scala +++ b/tests/printing/transformed/lazy-vals-legacy.scala @@ -1,3 +1,3 @@ object A { lazy val x: Int = 2 -} \ No newline at end of file +} diff --git a/tests/printing/transformed/lazy-vals-new.check b/tests/printing/transformed/lazy-vals-new.check index b0cf2733032f..521f5a6e51a6 100644 --- a/tests/printing/transformed/lazy-vals-new.check +++ b/tests/printing/transformed/lazy-vals-new.check @@ -22,20 +22,20 @@ package { ) private def writeReplace(): Object = new scala.runtime.ModuleSerializationProxy(classOf[A]) - @volatile lazy var x$lzy1: Object = null + @volatile private lazy var x$lzy1: Object = null lazy def x(): Int = { - var result: Object = A#x$lzy1 - if result.isInstanceOf[Int] then return scala.Int.unbox(result) else - if result.==(scala.runtime.LazyVals.NullValue) then - return scala.Int.unbox(null) - else return scala.Int.unbox(A.x$lzyINIT1()) + val result: Object = A#x$lzy1 + if result.isInstanceOf[Int] then scala.Int.unbox(result) else + if result.eq(scala.runtime.LazyVals.NullValue) then + scala.Int.unbox(null) + else scala.Int.unbox(A.x$lzyINIT1()) } private def x$lzyINIT1(): Object = while do { val current: Object = A#x$lzy1 - if current.==(null) then + if current.eq(null) then if scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, null, scala.runtime.LazyVals.Evaluating @@ -47,31 +47,29 @@ package { try { resultNullable = scala.Int.box(2) - if resultNullable.==(null) then + if resultNullable.eq(null) then result = scala.runtime.LazyVals.NullValue else result = resultNullable - return resultNullable + () } finally - { - if + if + scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, + scala.runtime.LazyVals.Evaluating + , result).unary_!() + then + { + val lock: scala.runtime.LazyVals.LazyVals$Waiting = + A#x$lzy1.asInstanceOf[ + scala.runtime.LazyVals.LazyVals$Waiting + ] scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, - scala.runtime.LazyVals.Evaluating - , result).unary_!() - then - { - val lock: scala.runtime.LazyVals.LazyVals$Waiting = - A#x$lzy1.asInstanceOf[ - scala.runtime.LazyVals.LazyVals$Waiting - ] - scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0 - , - lock, result) - lock.countDown() - } - else () - () - } + lock + , result) + lock.countDown() + } + else () + return resultNullable } else () else @@ -80,10 +78,10 @@ package { scala.runtime.LazyVals.LazyVals$LazyValControlState ] then - if current.==(scala.runtime.LazyVals.Evaluating) then + if current.eq(scala.runtime.LazyVals.Evaluating) then { scala.runtime.LazyVals.objCAS(classOf[A], A.OFFSET$_m_0, - scala.runtime.LazyVals.Evaluating + current , new scala.runtime.LazyVals.LazyVals$Waiting()) () } @@ -94,10 +92,7 @@ package { current.asInstanceOf[scala.runtime.LazyVals.LazyVals$Waiting]. await () - else - if current.==(scala.runtime.LazyVals.NullValue) then - return null - else () + else return null else return current } } diff --git a/tests/printing/transformed/lazy-vals-new.flags b/tests/printing/transformed/lazy-vals-new.flags index e6c43fc96531..51b77a4da919 100644 --- a/tests/printing/transformed/lazy-vals-new.flags +++ b/tests/printing/transformed/lazy-vals-new.flags @@ -1 +1 @@ --Ylightweight-lazy-vals \ No newline at end of file +-Ylightweight-lazy-vals diff --git a/tests/printing/transformed/lazy-vals-new.scala b/tests/printing/transformed/lazy-vals-new.scala index 28387acec9c5..7240286af56a 100644 --- a/tests/printing/transformed/lazy-vals-new.scala +++ b/tests/printing/transformed/lazy-vals-new.scala @@ -1,3 +1,3 @@ object A { lazy val x: Int = 2 -} \ No newline at end of file +} diff --git a/tests/run/lazyVals_c3.1.0.scala b/tests/run/lazyVals_c3.1.0.scala index 45e796ab46d3..8d36bd4a8825 100644 --- a/tests/run/lazyVals_c3.1.0.scala +++ b/tests/run/lazyVals_c3.1.0.scala @@ -10,4 +10,4 @@ class Foo: @main def Test = val foo = new Foo println(foo.x) - println(foo.y) \ No newline at end of file + println(foo.y)