Skip to content

Commit b8ad7b1

Browse files
authored
Refactor init checker: Extract reusable code (#16705)
Refactor init checker: Extract reusable code
2 parents c0a7d12 + 464fa5d commit b8ad7b1

File tree

5 files changed

+451
-323
lines changed

5 files changed

+451
-323
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package dotty.tools.dotc
2+
package transform
3+
package init
4+
5+
import core.*
6+
import Contexts.*
7+
8+
import ast.tpd
9+
import tpd.Tree
10+
11+
/** The co-inductive cache used for analysis
12+
*
13+
* The cache contains two maps from `(Config, Tree)` to `Res`:
14+
*
15+
* - input cache (`this.last`)
16+
* - output cache (`this.current`)
17+
*
18+
* The two caches are required because we want to make sure in a new iteration,
19+
* an expression is evaluated exactly once. The monotonicity of the analysis
20+
* ensures that the cache state goes up the lattice of the abstract domain,
21+
* consequently the algorithm terminates.
22+
*
23+
* The general skeleton for usage of the cache is as follows
24+
*
25+
* def analysis(entryExp: Expr) = {
26+
* def iterate(entryExp: Expr)(using Cache) =
27+
* eval(entryExp, initConfig)
28+
* if cache.hasChanged && noErrors then
29+
* cache.last = cache.current
30+
* cache.current = Empty
31+
* cache.changed = false
32+
* iterate(entryExp)
33+
* else
34+
* reportErrors
35+
*
36+
*
37+
* def eval(expr: Expr, config: Config)(using Cache) =
38+
* cache.cachedEval(config, expr) {
39+
* // Actual recursive evaluation of expression.
40+
* //
41+
* // Only executed if the entry `(exp, config)` is not in the output cache.
42+
* }
43+
*
44+
* iterate(entryExp)(using new Cache)
45+
* }
46+
*
47+
* See the documentation for the method `Cache.cachedEval` for more information.
48+
*
49+
* What goes to the configuration (`Config`) and what goes to the result (`Res`)
50+
* need to be decided by the specific analysis and justified by reasoning about
51+
* soundness.
52+
*
53+
* @param Config The analysis state that matters for evaluating an expression.
54+
* @param Res The result from the evaluation the given expression.
55+
*/
56+
class Cache[Config, Res]:
57+
import Cache.*
58+
59+
/** The cache for expression values from last iteration */
60+
protected var last: ExprValueCache[Config, Res] = Map.empty
61+
62+
/** The output cache for expression values
63+
*
64+
* The output cache is computed based on the cache values `last` from the
65+
* last iteration.
66+
*
67+
* Both `last` and `current` are required to make sure an encountered
68+
* expression is evaluated once in each iteration.
69+
*/
70+
protected var current: ExprValueCache[Config, Res] = Map.empty
71+
72+
/** Whether the current heap is different from the last heap?
73+
*
74+
* `changed == false` implies that the fixed point has been reached.
75+
*/
76+
protected var changed: Boolean = false
77+
78+
/** Used to avoid allocation, its state does not matter */
79+
protected given MutableTreeWrapper = new MutableTreeWrapper
80+
81+
def get(config: Config, expr: Tree): Option[Res] =
82+
current.get(config, expr)
83+
84+
/** Evaluate an expression with cache
85+
*
86+
* The algorithmic skeleton is as follows:
87+
*
88+
* if this.current.contains(config, expr) then
89+
* return cached value
90+
* else
91+
* val assumed = this.last(config, expr) or bottom value if absent
92+
* this.current(config, expr) = assumed
93+
* val actual = eval(exp)
94+
*
95+
* if assumed != actual then
96+
* this.changed = true
97+
* this.current(config, expr) = actual
98+
*
99+
*/
100+
def cachedEval(config: Config, expr: Tree, cacheResult: Boolean, default: Res)(eval: Tree => Res): Res =
101+
this.get(config, expr) match
102+
case Some(value) => value
103+
case None =>
104+
val assumeValue: Res =
105+
this.last.get(config, expr) match
106+
case Some(value) => value
107+
case None =>
108+
this.last = this.last.updatedNested(config, expr, default)
109+
default
110+
111+
this.current = this.current.updatedNested(config, expr, assumeValue)
112+
113+
val actual = eval(expr)
114+
if actual != assumeValue then
115+
// println("Changed! from = " + assumeValue + ", to = " + actual)
116+
this.changed = true
117+
// TODO: respect cacheResult to reduce cache size
118+
this.current = this.current.updatedNested(config, expr, actual)
119+
// this.current = this.current.removed(config, expr)
120+
end if
121+
122+
actual
123+
end cachedEval
124+
125+
def hasChanged = changed
126+
127+
/** Prepare cache for the next iteration
128+
*
129+
* 1. Reset changed flag.
130+
*
131+
* 2. Use current cache as last cache and set current cache to be empty.
132+
*/
133+
def prepareForNextIteration()(using Context) =
134+
this.changed = false
135+
this.last = this.current
136+
this.current = Map.empty
137+
end Cache
138+
139+
object Cache:
140+
type ExprValueCache[Config, Res] = Map[Config, Map[TreeWrapper, Res]]
141+
142+
/** A wrapper for trees for storage in maps based on referential equality of trees. */
143+
abstract class TreeWrapper:
144+
def tree: Tree
145+
146+
override final def equals(other: Any): Boolean =
147+
other match
148+
case that: TreeWrapper => this.tree eq that.tree
149+
case _ => false
150+
151+
override final def hashCode = tree.hashCode
152+
153+
/** The immutable wrapper is intended to be stored as key in the heap. */
154+
class ImmutableTreeWrapper(val tree: Tree) extends TreeWrapper
155+
156+
/** For queries on the heap, reuse the same wrapper to avoid unnecessary allocation.
157+
*
158+
* A `MutableTreeWrapper` is only ever used temporarily for querying a map,
159+
* and is never inserted to the map.
160+
*/
161+
class MutableTreeWrapper extends TreeWrapper:
162+
var queryTree: Tree | Null = null
163+
def tree: Tree = queryTree match
164+
case tree: Tree => tree
165+
case null => ???
166+
167+
extension [Config, Res](cache: ExprValueCache[Config, Res])
168+
def get(config: Config, expr: Tree)(using queryWrapper: MutableTreeWrapper): Option[Res] =
169+
queryWrapper.queryTree = expr
170+
cache.get(config).flatMap(_.get(queryWrapper))
171+
172+
def removed(config: Config, expr: Tree)(using queryWrapper: MutableTreeWrapper) =
173+
queryWrapper.queryTree = expr
174+
val innerMap2 = cache(config).removed(queryWrapper)
175+
cache.updated(config, innerMap2)
176+
177+
def updatedNested(config: Config, expr: Tree, result: Res): ExprValueCache[Config, Res] =
178+
val wrapper = new ImmutableTreeWrapper(expr)
179+
updatedNestedWrapper(config, wrapper, result)
180+
181+
def updatedNestedWrapper(config: Config, wrapper: ImmutableTreeWrapper, result: Res): ExprValueCache[Config, Res] =
182+
val innerMap = cache.getOrElse(config, Map.empty[TreeWrapper, Res])
183+
val innerMap2 = innerMap.updated(wrapper, result)
184+
cache.updated(config, innerMap2)
185+
end extension

compiler/src/dotty/tools/dotc/transform/init/Errors.scala

+13-58
Original file line numberDiff line numberDiff line change
@@ -5,106 +5,61 @@ package init
55

66
import ast.tpd._
77
import core._
8-
import util.SourcePosition
98
import util.Property
10-
import Decorators._, printing.SyntaxHighlighting
9+
import util.SourcePosition
1110
import Types._, Symbols._, Contexts._
1211

13-
import scala.collection.mutable
12+
import Trace.Trace
1413

1514
object Errors:
1615
private val IsFromPromotion = new Property.Key[Boolean]
1716

1817
sealed trait Error:
19-
def trace: Seq[Tree]
18+
def trace: Trace
2019
def show(using Context): String
2120

22-
def pos(using Context): SourcePosition = trace.last.sourcePos
21+
def pos(using Context): SourcePosition = Trace.position(using trace).sourcePos
2322

2423
def stacktrace(using Context): String =
2524
val preamble: String =
2625
if ctx.property(IsFromPromotion).nonEmpty
2726
then " Promotion trace:\n"
2827
else " Calling trace:\n"
29-
buildStacktrace(trace, preamble)
28+
Trace.buildStacktrace(trace, preamble)
3029

3130
def issue(using Context): Unit =
3231
report.warning(show, this.pos)
3332
end Error
3433

35-
def buildStacktrace(trace: Seq[Tree], preamble: String)(using Context): String = if trace.isEmpty then "" else preamble + {
36-
var lastLineNum = -1
37-
var lines: mutable.ArrayBuffer[String] = new mutable.ArrayBuffer
38-
trace.foreach { tree =>
39-
val pos = tree.sourcePos
40-
val prefix = "-> "
41-
val line =
42-
if pos.source.exists then
43-
val loc = "[ " + pos.source.file.name + ":" + (pos.line + 1) + " ]"
44-
val code = SyntaxHighlighting.highlight(pos.lineContent.trim.nn)
45-
i"$code\t$loc"
46-
else
47-
tree.show
48-
val positionMarkerLine =
49-
if pos.exists && pos.source.exists then
50-
positionMarker(pos)
51-
else ""
52-
53-
// always use the more precise trace location
54-
if lastLineNum == pos.line then
55-
lines.dropRightInPlace(1)
56-
57-
lines += (prefix + line + "\n" + positionMarkerLine)
58-
59-
lastLineNum = pos.line
60-
}
61-
val sb = new StringBuilder
62-
for line <- lines do sb.append(line)
63-
sb.toString
64-
}
65-
66-
/** Used to underline source positions in the stack trace
67-
* pos.source must exist
68-
*/
69-
private def positionMarker(pos: SourcePosition): String =
70-
val trimmed = pos.lineContent.takeWhile(c => c.isWhitespace).length
71-
val padding = pos.startColumnPadding.substring(trimmed).nn + " "
72-
val carets =
73-
if (pos.startLine == pos.endLine)
74-
"^" * math.max(1, pos.endColumn - pos.startColumn)
75-
else "^"
76-
77-
s"$padding$carets\n"
78-
7934
override def toString() = this.getClass.getName.nn
8035

8136
/** Access non-initialized field */
82-
case class AccessNonInit(field: Symbol)(val trace: Seq[Tree]) extends Error:
83-
def source: Tree = trace.last
37+
case class AccessNonInit(field: Symbol)(val trace: Trace) extends Error:
38+
def source: Tree = Trace.position(using trace)
8439
def show(using Context): String =
8540
"Access non-initialized " + field.show + "." + stacktrace
8641

8742
override def pos(using Context): SourcePosition = field.sourcePos
8843

8944
/** Promote a value under initialization to fully-initialized */
90-
case class PromoteError(msg: String)(val trace: Seq[Tree]) extends Error:
45+
case class PromoteError(msg: String)(val trace: Trace) extends Error:
9146
def show(using Context): String = msg + stacktrace
9247

93-
case class AccessCold(field: Symbol)(val trace: Seq[Tree]) extends Error:
48+
case class AccessCold(field: Symbol)(val trace: Trace) extends Error:
9449
def show(using Context): String =
9550
"Access field " + field.show + " on a cold object." + stacktrace
9651

97-
case class CallCold(meth: Symbol)(val trace: Seq[Tree]) extends Error:
52+
case class CallCold(meth: Symbol)(val trace: Trace) extends Error:
9853
def show(using Context): String =
9954
"Call method " + meth.show + " on a cold object." + stacktrace
10055

101-
case class CallUnknown(meth: Symbol)(val trace: Seq[Tree]) extends Error:
56+
case class CallUnknown(meth: Symbol)(val trace: Trace) extends Error:
10257
def show(using Context): String =
10358
val prefix = if meth.is(Flags.Method) then "Calling the external method " else "Accessing the external field"
10459
prefix + meth.show + " may cause initialization errors." + stacktrace
10560

10661
/** Promote a value under initialization to fully-initialized */
107-
case class UnsafePromotion(msg: String, error: Error)(val trace: Seq[Tree]) extends Error:
62+
case class UnsafePromotion(msg: String, error: Error)(val trace: Trace) extends Error:
10863
def show(using Context): String =
10964
msg + stacktrace + "\n" +
11065
"Promoting the value to hot (transitively initialized) failed due to the following problem:\n" + {
@@ -116,7 +71,7 @@ object Errors:
11671
*
11772
* Invariant: argsIndices.nonEmpty
11873
*/
119-
case class UnsafeLeaking(error: Error, nonHotOuterClass: Symbol, argsIndices: List[Int])(val trace: Seq[Tree]) extends Error:
74+
case class UnsafeLeaking(error: Error, nonHotOuterClass: Symbol, argsIndices: List[Int])(val trace: Trace) extends Error:
12075
def show(using Context): String =
12176
"Problematic object instantiation: " + argumentInfo() + stacktrace + "\n" +
12277
"It leads to the following error during object initialization:\n" +

0 commit comments

Comments
 (0)