Skip to content

Commit

Permalink
Handle local lazy vals properly (#18998)
Browse files Browse the repository at this point in the history
Lazy vals should be evaluated lazily. In the abstract semantics, we
over-approximate lazy vals by treating them as parameter-less methods.
The abstract cache will guard against non-terminating recursion and
avoids unnecessary re-evaluation.

The checker for class instances already handles local lazy vals
properly.
  • Loading branch information
liufengyun authored Nov 22, 2023
2 parents 6b69c39 + ba88a96 commit 39ec69f
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 17 deletions.
40 changes: 23 additions & 17 deletions compiler/src/dotty/tools/dotc/transform/init/Objects.scala
Original file line number Diff line number Diff line change
Expand Up @@ -947,27 +947,32 @@ object Objects:
Bottom
end if
case _ =>
// Only vals can be lazy
report.warning("[Internal error] Variable not found " + sym.show + "\nenv = " + env.show + ". " + Trace.show, Trace.position)
Bottom
else
given Env.Data = env
// Assume forward reference check is doing a good job
val value = Env.valValue(sym)
if isByNameParam(sym) then
value match
case fun: Fun =>
given Env.Data = fun.env
eval(fun.code, fun.thisV, fun.klass)
case Cold =>
report.warning("Calling cold by-name alias. " + Trace.show, Trace.position)
Bottom
case _: ValueSet | _: Ref | _: OfArray =>
report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position)
Bottom
if sym.is(Flags.Lazy) then
val rhs = sym.defTree.asInstanceOf[ValDef].rhs
eval(rhs, thisV, sym.enclosingClass.asClass, cacheResult = true)
else
value
// Assume forward reference check is doing a good job
val value = Env.valValue(sym)
if isByNameParam(sym) then
value match
case fun: Fun =>
given Env.Data = fun.env
eval(fun.code, fun.thisV, fun.klass)
case Cold =>
report.warning("Calling cold by-name alias. " + Trace.show, Trace.position)
Bottom
case _: ValueSet | _: Ref | _: OfArray =>
report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position)
Bottom
else
value

case _ =>
case None =>
if isByNameParam(sym) then
report.warning("Calling cold by-name alias. " + Trace.show, Trace.position)
Bottom
Expand Down Expand Up @@ -1232,9 +1237,10 @@ object Objects:

case vdef : ValDef =>
// local val definition
val rhs = eval(vdef.rhs, thisV, klass)
val sym = vdef.symbol
initLocal(vdef.symbol, rhs)
if !sym.is(Flags.Lazy) then
val rhs = eval(vdef.rhs, thisV, klass)
initLocal(sym, rhs)
Bottom

case ddef : DefDef =>
Expand Down
19 changes: 19 additions & 0 deletions tests/init-global/neg/lazy-local-val.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
object A:
class Box(value: => Int)

def f(a: => Int): Box =
val b = a
Box(b)

val box = f(n) // error
val n = 10

object B:
class Box(value: Int)

def f(a: => Int): Box =
lazy val b = a
Box(b)

val box = f(n) // error
val n = 10
91 changes: 91 additions & 0 deletions tests/init-global/pos/i18628-lazy.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
abstract class Reader[+T] {
def first: T

def rest: Reader[T]

def atEnd: Boolean
}

trait Parsers {
type Elem
type Input = Reader[Elem]

sealed abstract class ParseResult[+T] {
val successful: Boolean

def map[U](f: T => U): ParseResult[U]

def flatMapWithNext[U](f: T => Input => ParseResult[U]): ParseResult[U]
}

sealed abstract class NoSuccess(val msg: String) extends ParseResult[Nothing] { // when we don't care about the difference between Failure and Error
val successful = false

def map[U](f: Nothing => U) = this

def flatMapWithNext[U](f: Nothing => Input => ParseResult[U]): ParseResult[U]
= this
}

case class Failure(override val msg: String) extends NoSuccess(msg)

case class Error(override val msg: String) extends NoSuccess(msg)

case class Success[+T](result: T, val next: Input) extends ParseResult[T] {
val successful = true

def map[U](f: T => U) = Success(f(result), next)

def flatMapWithNext[U](f: T => Input => ParseResult[U]): ParseResult[U] = f(result)(next) match {
case s @ Success(result, rest) => Success(result, rest)
case f: Failure => f
case e: Error => e
}
}

case class ~[+a, +b](_1: a, _2: b) {
override def toString = s"(${_1}~${_2})"
}

abstract class Parser[+T] extends (Input => ParseResult[T]) {
def apply(in: Input): ParseResult[T]

def ~ [U](q: => Parser[U]): Parser[~[T, U]] = { lazy val p = q
(for(a <- this; b <- p) yield new ~(a,b))
}

def flatMap[U](f: T => Parser[U]): Parser[U]
= Parser{ in => this(in) flatMapWithNext(f)}

def map[U](f: T => U): Parser[U] //= flatMap{x => success(f(x))}
= Parser{ in => this(in) map(f)}

def ^^ [U](f: T => U): Parser[U] = map(f)
}

def Parser[T](f: Input => ParseResult[T]): Parser[T]
= new Parser[T]{ def apply(in: Input) = f(in) }

def accept(e: Elem): Parser[Elem] = acceptIf(_ == e)("'"+e+"' expected but " + _ + " found")

def acceptIf(p: Elem => Boolean)(err: Elem => String): Parser[Elem] = Parser { in =>
if (in.atEnd) Failure("end of input")
else if (p(in.first)) Success(in.first, in.rest)
else Failure(err(in.first))
}
}


object grammars3 extends Parsers {
type Elem = String

val a: Parser[String] = accept("a")
val b: Parser[String] = accept("b")

val AnBnCn: Parser[List[String]] = {
repMany(a,b)
}

def repMany[T](p: => Parser[T], q: => Parser[T]): Parser[List[T]] =
p~repMany(p,q)~q ^^ {case x~xs~y => x::xs:::(y::Nil)}
}
9 changes: 9 additions & 0 deletions tests/init-global/pos/lazy-local-val.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
object Test:
class Box(value: => Int)

def f(a: => Int): Box =
lazy val b = a
Box(b)

val box = f(n)
val n = 10
7 changes: 7 additions & 0 deletions tests/init-global/pos/lazy-local-val2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
object C:
def f(a: => Int): Int =
lazy val a: Int = 10 + b
lazy val b: Int = 20 + a
b

val n = f(10)

0 comments on commit 39ec69f

Please sign in to comment.