Skip to content

Commit ed42813

Browse files
committed
Make best-effort fail gracefully
Makes it so the best-effort compilation does not crash during the pickling and unpickling phase (mostly to avoid unnecessary crash reports from reading metals logs, where those may appear), instead providing a more descriptive error message related to the feature. Also adjusts the testing framework for best effort to look out for those errors.
1 parent 2c2a279 commit ed42813

File tree

4 files changed

+129
-74
lines changed

4 files changed

+129
-74
lines changed

compiler/src/dotty/tools/dotc/Driver.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class Driver {
3939
catch
4040
case ex: FatalError =>
4141
report.error(ex.getMessage.nn) // signals that we should fail compilation.
42+
case ex: Throwable if ctx.usesBestEffortTasty =>
43+
report.bestEffortError(ex, "Some best-effort tasty files were not able to be used.")
4244
case ex: TypeError if !runOrNull.enrichedErrorMessage =>
4345
println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}"))
4446
throw ex

compiler/src/dotty/tools/dotc/report.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ object report:
8080
if ctx.settings.YdebugError.value then Thread.dumpStack()
8181
if ctx.settings.YdebugTypeError.value then ex.printStackTrace()
8282

83+
def bestEffortError(ex: Throwable, msg: String)(using Context): Unit =
84+
// Build tools and our test framework may check precisely for
85+
// "Unsuccessful best-effort compilation." error text.
86+
val fullMsg =
87+
em"""Unsuccessful best-effort compilation.
88+
|${msg}
89+
|Cause:
90+
| ${ex.toString.replace("\n", "\n ")}
91+
|Stack trace:
92+
| ${ex.getStackTrace().mkString("\n ")}"""
93+
ctx.reporter.report(new Error(fullMsg, NoSourcePosition))
94+
8395
def errorOrMigrationWarning(msg: Message, pos: SrcPos, from: SourceVersion)(using Context): Unit =
8496
if sourceVersion.isAtLeast(from) then
8597
if sourceVersion.isMigrating && sourceVersion.ordinal <= from.ordinal then migrationWarning(msg, pos)

compiler/src/dotty/tools/dotc/transform/Pickler.scala

Lines changed: 68 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import util.concurrent.{Executor, Future}
1818
import compiletime.uninitialized
1919
import java.nio.file.Files
2020
import scala.language.unsafeNulls
21+
import scala.util.control.NonFatal
2122

2223
object Pickler {
2324
val name: String = "pickler"
@@ -40,7 +41,7 @@ class Pickler extends Phase {
4041

4142
// No need to repickle trees coming from TASTY
4243
override def isRunnable(using Context): Boolean =
43-
super.isRunnable && !ctx.settings.fromTasty.value
44+
super.isRunnable && !ctx.settings.fromTasty.value && (ctx.isBestEffort || !ctx.usesBestEffortTasty)
4445

4546
private def output(name: String, msg: String) = {
4647
val s = new PrintStream(name)
@@ -75,75 +76,78 @@ class Pickler extends Phase {
7576
private val executor = Executor[Array[Byte]]()
7677

7778
private def useExecutor(using Context) =
78-
Pickler.ParallelPickling && !ctx.settings.YtestPickler.value
79+
Pickler.ParallelPickling && !ctx.settings.YtestPickler.value && !ctx.isBestEffort
7980

8081
override def run(using Context): Unit = {
8182
val unit = ctx.compilationUnit
8283
val isBestEffort = ctx.reporter.errorsReported || ctx.usesBestEffortTasty
8384
pickling.println(i"unpickling in run ${ctx.runId}")
84-
85-
for
86-
cls <- dropCompanionModuleClasses(topLevelClasses(unit.tpdTree))
87-
tree <- sliceTopLevel(unit.tpdTree, cls)
88-
do
89-
if ctx.settings.YtestPickler.value then beforePickling(cls) = tree.show
90-
91-
val pickler = new TastyPickler(cls)
92-
val treePkl = new TreePickler(pickler)
93-
treePkl.pickle(tree :: Nil)
94-
Profile.current.recordTasty(treePkl.buf.length)
95-
96-
val positionWarnings = new mutable.ListBuffer[Message]()
97-
def reportPositionWarnings() = positionWarnings.foreach(report.warning(_))
98-
99-
def computePickled(): Array[Byte] = inContext(ctx.fresh) {
100-
serialized.run { scratch =>
101-
treePkl.compactify(scratch)
102-
if tree.span.exists then
103-
val reference = ctx.settings.sourceroot.value
104-
PositionPickler.picklePositions(
105-
pickler, treePkl.buf.addrOfTree, treePkl.treeAnnots, reference,
106-
unit.source, tree :: Nil, positionWarnings,
107-
scratch.positionBuffer, scratch.pickledIndices)
108-
109-
if !ctx.settings.YdropComments.value then
110-
CommentPickler.pickleComments(
111-
pickler, treePkl.buf.addrOfTree, treePkl.docString, tree,
112-
scratch.commentBuffer)
113-
114-
val pickled = pickler.assembleParts(isBestEffort)
115-
116-
def rawBytes = // not needed right now, but useful to print raw format.
117-
pickled.iterator.grouped(10).toList.zipWithIndex.map {
118-
case (row, i) => s"${i}0: ${row.mkString(" ")}"
119-
}
120-
121-
// println(i"rawBytes = \n$rawBytes%\n%") // DEBUG
122-
if pickling ne noPrinter then
123-
println(i"**** pickled info of $cls")
124-
println(TastyPrinter.showContents(pickled, ctx.settings.color.value == "never"))
125-
pickled
85+
try
86+
for
87+
cls <- dropCompanionModuleClasses(topLevelClasses(unit.tpdTree))
88+
tree <- sliceTopLevel(unit.tpdTree, cls)
89+
do
90+
if ctx.settings.YtestPickler.value then beforePickling(cls) = tree.show
91+
92+
val pickler = new TastyPickler(cls)
93+
val treePkl = new TreePickler(pickler)
94+
treePkl.pickle(tree :: Nil)
95+
Profile.current.recordTasty(treePkl.buf.length)
96+
97+
val positionWarnings = new mutable.ListBuffer[Message]()
98+
def reportPositionWarnings() = positionWarnings.foreach(report.warning(_))
99+
100+
def computePickled(): Array[Byte] = inContext(ctx.fresh) {
101+
serialized.run { scratch =>
102+
treePkl.compactify(scratch)
103+
if tree.span.exists then
104+
val reference = ctx.settings.sourceroot.value
105+
PositionPickler.picklePositions(
106+
pickler, treePkl.buf.addrOfTree, treePkl.treeAnnots, reference,
107+
unit.source, tree :: Nil, positionWarnings,
108+
scratch.positionBuffer, scratch.pickledIndices)
109+
110+
if !ctx.settings.YdropComments.value then
111+
CommentPickler.pickleComments(
112+
pickler, treePkl.buf.addrOfTree, treePkl.docString, tree,
113+
scratch.commentBuffer)
114+
115+
val pickled = pickler.assembleParts(isBestEffort)
116+
117+
def rawBytes = // not needed right now, but useful to print raw format.
118+
pickled.iterator.grouped(10).toList.zipWithIndex.map {
119+
case (row, i) => s"${i}0: ${row.mkString(" ")}"
120+
}
121+
122+
// println(i"rawBytes = \n$rawBytes%\n%") // DEBUG
123+
if pickling ne noPrinter then
124+
println(i"**** pickled info of $cls")
125+
println(TastyPrinter.showContents(pickled, ctx.settings.color.value == "never"))
126+
pickled
127+
}
126128
}
127-
}
128129

129-
/** A function that returns the pickled bytes. Depending on `Pickler.ParallelPickling`
130-
* either computes the pickled data in a future or eagerly before constructing the
131-
* function value.
132-
*/
133-
val demandPickled: () => Array[Byte] =
134-
if useExecutor then
135-
val futurePickled = executor.schedule(computePickled)
136-
() =>
137-
try futurePickled.force.get
138-
finally reportPositionWarnings()
139-
else
140-
val pickled = computePickled()
141-
reportPositionWarnings()
142-
if ctx.settings.YtestPickler.value then pickledBytes(cls) = pickled
143-
() => pickled
144-
145-
unit.pickled += (cls -> demandPickled)
146-
end for
130+
/** A function that returns the pickled bytes. Depending on `Pickler.ParallelPickling`
131+
* either computes the pickled data in a future or eagerly before constructing the
132+
* function value.
133+
*/
134+
val demandPickled: () => Array[Byte] =
135+
if useExecutor then
136+
val futurePickled = executor.schedule(computePickled)
137+
() =>
138+
try futurePickled.force.get
139+
finally reportPositionWarnings()
140+
else
141+
val pickled = computePickled()
142+
reportPositionWarnings()
143+
if ctx.settings.YtestPickler.value then pickledBytes(cls) = pickled
144+
() => pickled
145+
146+
unit.pickled += (cls -> demandPickled)
147+
end for
148+
catch
149+
case NonFatal(ex) =>
150+
report.bestEffortError(ex, "Some best-effort tasty files will not be generated.")
147151
}
148152

149153
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = {
@@ -168,7 +172,7 @@ class Pickler extends Phase {
168172
.resolve("META-INF")
169173
.resolve("best-effort")
170174
Files.createDirectories(outpath)
171-
BestEffortTastyWriter.write(outpath.nn, units)
175+
BestEffortTastyWriter.write(outpath.nn, result)
172176
result
173177
}
174178

compiler/test/dotty/tools/vulpix/ParallelTesting.scala

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,6 @@ trait ParallelTesting extends RunnerOrchestration { self =>
657657
val flags = flags0 and "-Ywith-best-effort-tasty"
658658
val reporter = mkReporter
659659
val driver = new Driver
660-
println((flags.all ++ files0.map(_.toString) ++ Array(bestEffortDir.toString)).mkString(" "))
661660

662661
val args = Array("-classpath", flags.defaultClassPath + ":" + bestEffortDir.toString) ++ flags.options
663662

@@ -913,6 +912,30 @@ trait ParallelTesting extends RunnerOrchestration { self =>
913912
override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] = None
914913
}
915914

915+
private final class NoBestEffortErrorsTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
916+
extends Test(testSources, times, threadLimit, suppressAllOutput) {
917+
918+
override def suppressErrors = true
919+
920+
override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] =
921+
def compilerCrashed = reporters.exists(_.compilerCrashed)
922+
val unsucceffulBestEffortErrorMsg = "Unsuccessful best-effort compilation."
923+
val failedBestEffortCompilation: Seq[TestReporter] =
924+
reporters.collect{
925+
case testReporter if testReporter.errors.exists(_.msg.message.startsWith(unsucceffulBestEffortErrorMsg)) =>
926+
testReporter
927+
}
928+
929+
Option {
930+
if compilerCrashed then s"Compiler crashed when compiling: ${testSource.title}"
931+
else if !failedBestEffortCompilation.isEmpty then
932+
failedBestEffortCompilation.flatMap(_.consoleOutput.split("\n")).mkString("\n")
933+
else null
934+
}
935+
end maybeFailureMessage
936+
937+
}
938+
916939

917940
/** The `CompilationTest` is the main interface to `ParallelTesting`, it
918941
* can be instantiated via one of the following methods:
@@ -1061,12 +1084,28 @@ trait ParallelTesting extends RunnerOrchestration { self =>
10611084
this
10621085
}
10631086

1087+
/** Creates a "neg" test run, which makes sure that each test manages successful
1088+
* best-effort compilation, without any errors related to pickling/unpickling
1089+
* of betasty files.
1090+
*/
1091+
def checkNoBestEffortError()(implicit summaryReport: SummaryReporting): this.type = {
1092+
val test = new NoBestEffortErrorsTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput).executeTestSuite()
1093+
1094+
cleanup()
1095+
1096+
if (test.didFail) {
1097+
fail("Best-effort test should not have shown a \"Unsuccessful best-effort compilation\" error, but did")
1098+
}
1099+
1100+
this
1101+
}
1102+
10641103
/** Creates a "neg" test run, which makes sure that each test generates the
10651104
* correct number of errors at the correct positions. It also makes sure
10661105
* that none of these tests crashes the compiler.
10671106
*/
10681107
def checkExpectedErrors()(implicit summaryReport: SummaryReporting): this.type =
1069-
val test = new NegTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput).executeTestSuite()
1108+
val test = new NegTest(targets, times, threadLimit, shouldSuppressOutput).executeTestSuite()
10701109

10711110
cleanup()
10721111

@@ -1490,8 +1529,6 @@ trait ParallelTesting extends RunnerOrchestration { self =>
14901529
val (dirsStep1, filteredPicklingFiles) = compilationTargets(sourceDir, picklingFilter)
14911530
val (dirsStep2, filteredUnpicklingFiles) = compilationTargets(sourceDir, unpicklingFilter)
14921531

1493-
// val filteredPicklingFiles = f1//.slice(0, 50)
1494-
14951532
class BestEffortCompilation(
14961533
name: String,
14971534
file: JFile,
@@ -1593,13 +1630,13 @@ trait ParallelTesting extends RunnerOrchestration { self =>
15931630

15941631
val step1Compilation = JointCompilationSource(
15951632
testGroup.name, step1SourceFiles, flags.and("-Ybest-effort"), step1OutDir, fromTasty = NotFromTasty
1596-
)
1633+
)
15971634

15981635
val bestEffortDir = new JFile(step1OutDir, s"META-INF${JFile.separator}best-effort")
15991636

16001637
val step2Compilation = JointCompilationSource(
1601-
testGroup.name, step2SourceFiles, flags.and("-Ywith-best-effort-tasty"), step2OutDir, fromTasty = WithBestEffortTasty(bestEffortDir)
1602-
)
1638+
testGroup.name, step2SourceFiles, flags.and("-Ywith-best-effort-tasty").and("-YisBestEffort"), step2OutDir, fromTasty = WithBestEffortTasty(bestEffortDir)
1639+
)
16031640
(step1Compilation, step2Compilation, bestEffortDir)
16041641
}.unzip3
16051642

@@ -1641,8 +1678,8 @@ trait ParallelTesting extends RunnerOrchestration { self =>
16411678
class BestEffortCompilationTest(step1: CompilationTest, step2: CompilationTest, bestEffortDirs: List[JFile], shouldDelete: Boolean)(implicit testGroup: TestGroup) {
16421679

16431680
def checkNoCrash()(implicit summaryReport: SummaryReporting): this.type = {
1644-
step1.checkNoCrash() // Compile all files to generate the class files with best effort tasty
1645-
step2.checkNoCrash() // Compile with best effort tasty
1681+
step1.checkNoBestEffortError() // Compile all files to generate the class files with best effort tasty
1682+
step2.checkNoBestEffortError() // Compile with best effort tasty
16461683

16471684
if (shouldDelete) {
16481685
CompilationTest.aggregateTests(step1, step2).delete()
@@ -1660,7 +1697,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
16601697
}
16611698

16621699
def noCrashWithCompilingDependencies()(implicit summaryReport: SummaryReporting): this.type = {
1663-
step1.checkNoCrash() // Compile all files to generate the class files with best effort tasty
1700+
step1.checkNoBestEffortError() // Compile all files to generate the class files with best effort tasty
16641701
step2.checkCompile() // Compile with best effort tasty
16651702

16661703
this

0 commit comments

Comments
 (0)