Skip to content
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

Introduce -Xpatmat-analysis-timeout #21766

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
24 changes: 24 additions & 0 deletions compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
*/
@volatile var isCancelled = false

/** The timeout for pattern match exhaustivity analysis, in ms.
* When the timeout is reached, it is reduced for the next analysis (see "backoff").
* When the timeout is not reached, it is recovered (up to the original, see "recover").
* */
private var myExhaustivityAnalysisTimeout: Int =
Int.MinValue // sentinel value; means whatever is set in command line option

def exhaustivityAnalysisTimeout: Int =
if myExhaustivityAnalysisTimeout == Int.MinValue
then ctx.settings.XpatmatAnalysisTimeout.value
else myExhaustivityAnalysisTimeout

/** Exponentially back off, by halving on every timeout, to a minimum 100 ms. */
def backoffExhaustivityAnalysisTimeout(): Unit =
myExhaustivityAnalysisTimeout =
(exhaustivityAnalysisTimeout / 2)
.max(100)

/** Recover slowly, by 1.5 times, up to the original value. */
def recoverExhaustivityAnalysisTimeout(): Unit =
myExhaustivityAnalysisTimeout =
(exhaustivityAnalysisTimeout * 1.5).toInt
.min(ictx.settings.XpatmatAnalysisTimeout.value)

private var compiling = false

private var myUnits: List[CompilationUnit] = Nil
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ private sealed trait XSettings:
val XverifySignatures: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xverify-signatures", "Verify generic signatures in generated bytecode.")
val XignoreScala2Macros: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xignore-scala2-macros", "Ignore errors when compiling code that calls Scala2 macros, these will fail at runtime.")
val XimportSuggestionTimeout: Setting[Int] = IntSetting(AdvancedSetting, "Ximport-suggestion-timeout", "Timeout (in ms) for searching for import suggestions when errors are reported.", 8000)
val XpatmatAnalysisTimeout: Setting[Int] = IntSetting(AdvancedSetting, "Xpatmat-analysis-timeout", "Timeout (in ms) for match analysis.", 8 * 1000) // 8s
val Xsemanticdb: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xsemanticdb", "Store information in SemanticDB.", aliases = List("-Ysemanticdb"))
val XuncheckedJavaOutputVersion: Setting[String] = ChoiceSetting(AdvancedSetting, "Xunchecked-java-output-version", "target", "Emit bytecode for the specified version of the Java platform. This might produce bytecode that will break at runtime. Corresponds to -target flag in javac. When on JDK 9+, consider -java-output-version as a safer alternative.", ScalaSettingsProperties.supportedTargetVersions, "", aliases = List("-Xtarget", "--Xtarget"))
val XcheckMacros: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros"))
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/Formatting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ object Formatting {
given Show[Char] = ShowAny
given Show[Boolean] = ShowAny
given Show[Integer] = ShowAny
given Show[Long] = ShowAny
given Show[String] = ShowAny
given Show[Class[?]] = ShowAny
given Show[Throwable] = ShowAny
given Show[StringBuffer] = ShowAny
given Show[CompilationUnit] = ShowAny
given Show[Phases.Phase] = ShowAny
given Show[TyperState] = ShowAny
given Show[Unit] = ShowAny
given Show[config.ScalaVersion] = ShowAny
given Show[io.AbstractFile] = ShowAny
given Show[parsing.Scanners.Scanner] = ShowAny
Expand Down
9 changes: 8 additions & 1 deletion compiler/src/dotty/tools/dotc/reporting/trace.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ trait TraceSyntax:
(op: => T)(using Context): T =
if ctx.mode.is(Mode.Printing) || !isForced && (printer eq Printers.noPrinter) then op
else
val start = System.nanoTime
// Avoid evaluating question multiple time, since each evaluation
// may cause some extra logging output.
val q = question
Expand All @@ -109,7 +110,13 @@ trait TraceSyntax:
def finalize(msg: String) =
if !finalized then
ctx.base.indent -= 1
doLog(s"$margin$msg")
val stop = System.nanoTime
val diffNs = stop - start
val diffS = (diffNs / 1000 / 1000).toInt / 1000.0
if diffS > 0.1 then
doLog(s"$margin$msg (${"%.2f".format(diffS)} s)")
else
doLog(s"$margin$msg")
finalized = true
try
doLog(s"$margin$leading")
Expand Down
44 changes: 40 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ case class Prod(tp: Type, unappTp: TermRef, params: List[Space]) extends Space
case class Or(spaces: Seq[Space]) extends Space

object SpaceEngine {
val DeadlineKey = new Property.Key[Long]

def setupDeadline(using Context): Context =
val timeout = ctx.run.nn.exhaustivityAnalysisTimeout
if timeout >= 0 then
ctx.fresh.setProperty(DeadlineKey, System.currentTimeMillis() + timeout)
else
ctx

def isPastDeadline(using Context): Boolean =
ctx.property(DeadlineKey) match
case Some(deadline) => System.currentTimeMillis() > deadline
case _ => false

def simplify(space: Space)(using Context): Space = space.simplify
def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b)
def canDecompose(typ: Typ)(using Context): Boolean = typ.canDecompose
Expand Down Expand Up @@ -590,6 +604,7 @@ object SpaceEngine {

/** Whether the extractor covers the given type */
def covers(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): Boolean = trace(i"covers($unapp, $scrutineeTp, $argLen)") {
!isPastDeadline && (
SpaceEngine.isIrrefutable(unapp, argLen)
|| unapp.symbol == defn.TypeTest_unapply && {
val AppliedType(_, _ :: tp :: Nil) = unapp.prefix.widen.dealias: @unchecked
Expand All @@ -599,6 +614,7 @@ object SpaceEngine {
val AppliedType(_, tp :: Nil) = unapp.prefix.widen.dealias: @unchecked
scrutineeTp <:< tp
}
)
}

/** Decompose a type into subspaces -- assume the type can be decomposed */
Expand Down Expand Up @@ -666,13 +682,16 @@ object SpaceEngine {

val parts = children.map { sym =>
val sym1 = if (sym.is(ModuleClass)) sym.sourceModule else sym
val refined = trace(i"refineUsingParent($tp, $sym1, $mixins)")(TypeOps.refineUsingParent(tp, sym1, mixins))
val refined =
if isPastDeadline then NoType
else TypeOps.refineUsingParent(tp, sym1, mixins)

def inhabited(tp: Type): Boolean = tp.dealias match
case AndType(tp1, tp2) => !TypeComparer.provablyDisjoint(tp1, tp2)
case OrType(tp1, tp2) => inhabited(tp1) || inhabited(tp2)
case tp: RefinedType => inhabited(tp.parent)
case tp: TypeRef => inhabited(tp.prefix)
case NoType => false
case _ => true

if inhabited(refined) then refined
Expand Down Expand Up @@ -862,6 +881,21 @@ object SpaceEngine {
case _ => tp
})

def checkExhaustivityInDeadline(m: Match)(using Context): Unit = {
inContext(setupDeadline):
checkExhaustivity(m)
if isPastDeadline then
ctx.run.nn.backoffExhaustivityAnalysisTimeout()
val setting = ctx.settings.XpatmatAnalysisTimeout
report.warning(
em"""Match analysis requires more time than allowed. You can try:
| * doubling the timeout: ${setting.name}:${setting.value * 2}
| * disabling the timeout: ${setting.name}:-1
| * adding `: @unchecked` to the scrutinee""", m.srcPos)
else
ctx.run.nn.recoverExhaustivityAnalysisTimeout()
}

def checkExhaustivity(m: Match)(using Context): Unit = trace(i"checkExhaustivity($m)") {
val selTyp = toUnderlying(m.selector.tpe.stripUnsafeNulls()).dealias
val targetSpace = trace(i"targetSpace($selTyp)")(project(selTyp))
Expand All @@ -880,7 +914,8 @@ object SpaceEngine {

if uncovered.nonEmpty then
val deduped = dedup(uncovered)
report.warning(PatternMatchExhaustivity(deduped, m), m.selector)
if !isPastDeadline then
report.warning(PatternMatchExhaustivity(deduped, m), m.selector)
}

private def reachabilityCheckable(sel: Tree)(using Context): Boolean =
Expand Down Expand Up @@ -929,7 +964,8 @@ object SpaceEngine {
then {
val nullOnly = isNullable && i == len - 1 && isWildcardArg(pat)
val msg = if nullOnly then MatchCaseOnlyNullWarning() else MatchCaseUnreachable()
report.warning(msg, pat.srcPos)
if !isPastDeadline then
report.warning(msg, pat.srcPos)
}
deferred = Nil
}
Expand All @@ -941,6 +977,6 @@ object SpaceEngine {
}

def checkMatch(m: Match)(using Context): Unit =
if exhaustivityCheckable(m.selector) then checkExhaustivity(m)
if exhaustivityCheckable(m.selector) then checkExhaustivityInDeadline(m)
if reachabilityCheckable(m.selector) then checkReachability(m)
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ trait ImportSuggestions:
end importSuggestions

/** Reduce next timeout for import suggestions by the amount of time it took
* for current search, but but never less than to half of the previous budget.
* for current search, but never less than to half of the previous budget.
*/
private def reduceTimeBudget(used: Int)(using Context) =
val run = ctx.run.nn
Expand Down
Loading
Loading