Skip to content

More details on Lazy Vals #5342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/docs/reference/changed/lazy-vals-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
layout: doc-page
title: Changed: Lazy Vals - More Details
---

Dotty implements [Version 6](https://docs.scala-lang.org/sips/improved-lazy-val-initialization.html#version-6---no-synchronization-on-this-and-concurrent-initialization-of-fields)
of the [SIP-20] improved lazy vals initialization proposal.

## Motivation

The newly proposed lazy val initialization mechanism aims to eliminate the acquisition of resources
during the execution of the lazy val initializer block, thus reducing the possibility of a deadlock.
The concrete deadlock scenarios that the new lazy val initialization scheme eliminates are
summarized in the [SIP-20] document.

## Implementation

Given a lazy field of the form:

```scala
class Foo {
@volatile lazy val bar = <RHS>
}
```

The Dotty compiler will generate code equivalent to:

```scala
class Foo {
import dotty.runtime.LazyVals
var value_0: Int = _
var bitmap = 0
val bitmap_offset = LazyVals.getOffset(classOf[LazyCell], "bitmap")

def bar(): Int = {
var result: Int = 0
var retry: Boolean = true
var flag: Long = 0L
while (retry) {
flag = LazyVals.get(this, bitmap_offset)
LazyVals.STATE(flag, <field-id>) match {
case <state-0> =>
if (LazyVals.CAS(this, bitmap_offset, flag, <state-1>)) {
try result = <RHS>
catch {
case ex =>
LazyVals.setFlag(this, bitmap_offset, <state-0>, <field-id>)
throw ex
}
value_0 = result
LazyVals.setFlag(this, bitmap_offset, <state-3>, <field-id>)
retry = false
}
case <state-1> | <state-2> =>
LazyVals.wait4Notification(this, bitmap_offset, flag, <field-id>)
case <state-3> =>
retry = false
result = $target
}
}
result
}
}
```

The state of the lazy val `<state-i>` is represented with 4 values: 0, 1, 2 and 3. The state 0
represents a non-initialized lazy val. The state 1 represents a lazy val that is currently being
initialized by some thread. The state 2 denotes that there are concurrent readers of the lazy val.
The state 3 represents a lazy val that has been initialized. `<field-id>` is the id of the lazy
val. This id grows with the number of volatile lazy vals defined in the class.

## Note on recursive lazy vals

Ideally recursive lazy vals should be flagged as an error. The current behavior for `@volatile`
recursive lazy vals is undefined (initialization may result in a deadlock). Non `@volatile` lazy
vals behave as in Scala 2.

## Reference

* [SIP-20]

[SIP-20]: https://docs.scala-lang.org/sips/improved-lazy-val-initialization.html
22 changes: 15 additions & 7 deletions docs/docs/reference/changed/lazy-vals.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
---
layout: doc-page
title: Changed: Lazy Vals and @volatile
title: Changed: Lazy Vals
---

Lazy val initialization no longer guarantees safe publishing. This change was done
to avoid the performance overhead of thread synchonization because many lazy vals are only used in a single-threaded context.
Lazy field initialization no longer guarantees safe publishing. This change was done
to avoid the performance overhead of thread synchonization because most lazy vals are only used in
a single-threaded context.

To get back safe publishing you need to annotate a lazy val with `@volatile`. In
## Compatibility considerations

@volatile lazy val x = expr
To get back safe publishing (Scala 2's default) you need to annotate a lazy val with `@volatile`. In

it is guaranteed that readers of a lazy val will see the value of `x`
once it is assigned.
```scala
@volatile lazy val x = expr
```

it is guaranteed that readers of a lazy val will see the value of `x` once it is assigned.

Local lazy vals (i.e. defined inside methods) are left unchanged.

The [ScalaFix](https://scalacenter.github.io/scalafix/) rewrite tool
as well as Dotty's own `-language:Scala2 -rewrite` option will rewrite all normal
lazy vals to volatile ones in order to keep their semantics compatible
with current Scala's.

[More details](./lazy-vals-spec.html)
22 changes: 22 additions & 0 deletions tests/run/i1856.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
Iteration 6
Iteration 7
Iteration 8
Iteration 9
42
Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
Iteration 6
Iteration 7
Iteration 8
Iteration 9
42
27 changes: 27 additions & 0 deletions tests/run/i1856.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
object Test {
var count = 0
lazy val lzy: Int = {
if (count < 10) {
println(s"Iteration $count")
count += 1
lzy
} else 42
}

def lzy2 = {
var countLocal = 0
lazy val lzyLocal: Int = {
if (countLocal < 10) {
println(s"Iteration $countLocal")
countLocal += 1
lzyLocal
} else 42
}
lzyLocal
}

def main(args: Array[String]): Unit = {
println(lzy)
println(lzy2)
}
}