Skip to content

Commit a48ba99

Browse files
committed
Handle stack overflows in Pickler
The original error case #16500 showed an example of deeply nested code which blew up TreePickler with a StackOverflow. We now handle this situation and provide better error diagnostics - point to the closes enclosing definition that caused the SO when pickled - suggest to increase the stack size with -Xss
1 parent c48037c commit a48ba99

File tree

2 files changed

+47
-24
lines changed

2 files changed

+47
-24
lines changed

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,21 +96,23 @@ end RecursionOverflow
9696
*/
9797
// Beware: Since this object is only used when handling a StackOverflow, this code
9898
// cannot consume significant amounts of stack.
99-
object handleRecursive {
99+
object handleRecursive:
100+
inline def underlyingStackOverflowOrNull(exc: Throwable): Throwable | Null =
101+
var e: Throwable | Null = exc
102+
while e != null && !e.isInstanceOf[StackOverflowError] do e = e.getCause
103+
e
104+
100105
def apply(op: String, details: => String, exc: Throwable, weight: Int = 1)(using Context): Nothing =
101-
if (ctx.settings.YnoDecodeStacktraces.value)
106+
if ctx.settings.YnoDecodeStacktraces.value then
102107
throw exc
103-
else
104-
exc match {
105-
case _: RecursionOverflow =>
106-
throw new RecursionOverflow(op, details, exc, weight)
107-
case _ =>
108-
var e: Throwable | Null = exc
109-
while (e != null && !e.isInstanceOf[StackOverflowError]) e = e.getCause
110-
if (e != null) throw new RecursionOverflow(op, details, e, weight)
111-
else throw exc
112-
}
113-
}
108+
else exc match
109+
case _: RecursionOverflow =>
110+
throw new RecursionOverflow(op, details, exc, weight)
111+
case _ =>
112+
val so = underlyingStackOverflowOrNull(exc)
113+
if so != null then throw new RecursionOverflow(op, details, so, weight)
114+
else throw exc
115+
end handleRecursive
114116

115117
/**
116118
* This TypeError signals that completing denot encountered a cycle: it asked for denot.info (or similar),

compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import collection.mutable
2020
import reporting.{Profile, NoProfile}
2121
import dotty.tools.tasty.TastyFormat.ASTsSection
2222

23+
object TreePickler:
24+
class StackSizeExceeded(val mdef: tpd.MemberDef) extends Exception
2325

2426
class TreePickler(pickler: TastyPickler) {
2527
val buf: TreeBuffer = new TreeBuffer
2628
pickler.newSection(ASTsSection, buf)
2729
import buf._
2830
import pickler.nameBuffer.nameIndex
2931
import tpd._
32+
import TreePickler.*
3033

3134
private val symRefs = Symbols.MutableSymbolMap[Addr](256)
3235
private val forwardSymRefs = Symbols.MutableSymbolMap[List[Addr]]()
@@ -53,7 +56,7 @@ class TreePickler(pickler: TastyPickler) {
5356
def docString(tree: untpd.MemberDef): Option[Comment] =
5457
Option(docStrings.lookup(tree))
5558

56-
private def withLength(op: => Unit) = {
59+
private inline def withLength(inline op: Unit) = {
5760
val lengthAddr = reserveRef(relative = true)
5861
op
5962
fillRef(lengthAddr, currentAddr, relative = true)
@@ -328,16 +331,24 @@ class TreePickler(pickler: TastyPickler) {
328331
registerDef(sym)
329332
writeByte(tag)
330333
val addr = currentAddr
331-
withLength {
332-
pickleName(sym.name)
333-
pickleParams
334-
tpt match {
335-
case _: Template | _: Hole => pickleTree(tpt)
336-
case _ if tpt.isType => pickleTpt(tpt)
334+
try
335+
withLength {
336+
pickleName(sym.name)
337+
pickleParams
338+
tpt match {
339+
case _: Template | _: Hole => pickleTree(tpt)
340+
case _ if tpt.isType => pickleTpt(tpt)
341+
}
342+
pickleTreeUnlessEmpty(rhs)
343+
pickleModifiers(sym, mdef)
337344
}
338-
pickleTreeUnlessEmpty(rhs)
339-
pickleModifiers(sym, mdef)
340-
}
345+
catch
346+
case ex: Throwable =>
347+
if !ctx.settings.YnoDecodeStacktraces.value
348+
&& handleRecursive.underlyingStackOverflowOrNull(ex) != null then
349+
throw StackSizeExceeded(mdef)
350+
else
351+
throw ex
341352
if sym.is(Method) && sym.owner.isClass then
342353
profile.recordMethodSize(sym, currentAddr.index - addr.index, mdef.span)
343354
for
@@ -784,7 +795,17 @@ class TreePickler(pickler: TastyPickler) {
784795

785796
def pickle(trees: List[Tree])(using Context): Unit = {
786797
profile = Profile.current
787-
trees.foreach(tree => if (!tree.isEmpty) pickleTree(tree))
798+
for tree <- trees do
799+
try
800+
if !tree.isEmpty then pickleTree(tree)
801+
catch case ex: StackSizeExceeded =>
802+
report.error(
803+
em"""Recursion limit exceeded while pickling ${ex.mdef}
804+
|in ${ex.mdef.symbol.showLocated}.
805+
|You could try to increase the stacksize using the -Xss JVM option.
806+
|For the unprocessed stack trace, compile with -Yno-decode-stacktraces.""",
807+
ex.mdef.srcPos)
808+
788809
def missing = forwardSymRefs.keysIterator
789810
.map(sym => i"${sym.showLocated} (line ${sym.srcPos.line}) #${sym.id}")
790811
.toList

0 commit comments

Comments
 (0)