|
| 1 | +package dotty.tools |
| 2 | +package dotc |
| 3 | +package core |
| 4 | + |
| 5 | +import Types._, Contexts._, Symbols._, Decorators._ |
| 6 | +import util.Property |
| 7 | + |
| 8 | +/** A utility module to produce match type reduction traces in error messages. |
| 9 | + */ |
| 10 | +object MatchTypeTrace: |
| 11 | + |
| 12 | + private enum TraceEntry: |
| 13 | + case TryReduce(scrut: Type) |
| 14 | + case NoMatches(scrut: Type, cases: List[Type]) |
| 15 | + case Stuck(scrut: Type, stuckCase: Type, otherCases: List[Type]) |
| 16 | + import TraceEntry._ |
| 17 | + |
| 18 | + private class MatchTrace: |
| 19 | + var entries: List[TraceEntry] = Nil |
| 20 | + |
| 21 | + private val MatchTrace = new Property.Key[MatchTrace] |
| 22 | + |
| 23 | + /** Execute `op` and if it involves a failed match type reduction |
| 24 | + * return the trace of that reduction. Otherwise return the empty string. |
| 25 | + */ |
| 26 | + def record(op: Context ?=> Any)(using Context): String = |
| 27 | + val trace = new MatchTrace |
| 28 | + inContext(ctx.fresh.setProperty(MatchTrace, trace)) { |
| 29 | + op |
| 30 | + if trace.entries.isEmpty then "" |
| 31 | + else |
| 32 | + i""" |
| 33 | + | |
| 34 | + |Note: a match type could not be fully reduced: |
| 35 | + | |
| 36 | + |${trace.entries.reverse.map(explainEntry)}%\n%""" |
| 37 | + } |
| 38 | + |
| 39 | + /** Are we running an operation that records a match type trace? */ |
| 40 | + def isRecording(using Context): Boolean = |
| 41 | + ctx.property(MatchTrace).isDefined |
| 42 | + |
| 43 | + private def matchTypeFail(entry: TraceEntry)(using Context) = |
| 44 | + ctx.property(MatchTrace) match |
| 45 | + case Some(trace) => |
| 46 | + trace.entries match |
| 47 | + case (e: TryReduce) :: es => trace.entries = entry :: trace.entries |
| 48 | + case _ => |
| 49 | + case _ => |
| 50 | + |
| 51 | + /** Record a failure that scrutinee `scrut` does not match any case in `cases`. |
| 52 | + * Only the first failure is recorded. |
| 53 | + */ |
| 54 | + def noMatches(scrut: Type, cases: List[Type])(using Context) = |
| 55 | + matchTypeFail(NoMatches(scrut, cases)) |
| 56 | + |
| 57 | + /** Record a failure that scrutinee `scrut` does not match `stuckCase` but is |
| 58 | + * not disjoint from it either, which means that the remaining cases `otherCases` |
| 59 | + * cannot be visited. Only the first failure is recorded. |
| 60 | + */ |
| 61 | + def stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])(using Context) = |
| 62 | + matchTypeFail(Stuck(scrut, stuckCase, otherCases)) |
| 63 | + |
| 64 | + /** Record in the trace that we are trying to reduce `scrut` when performing `op` |
| 65 | + * If `op` succeeds the entry is removed after exit. If `op` fails, it stays. |
| 66 | + */ |
| 67 | + def recurseWith(scrut: Type)(op: => Type)(using Context): Type = |
| 68 | + ctx.property(MatchTrace) match |
| 69 | + case Some(trace) => |
| 70 | + val prev = trace.entries |
| 71 | + trace.entries = TryReduce(scrut) :: prev |
| 72 | + val res = op |
| 73 | + if res.exists then trace.entries = prev |
| 74 | + res |
| 75 | + case _ => |
| 76 | + op |
| 77 | + |
| 78 | + private def caseText(tp: Type)(using Context): String = tp match |
| 79 | + case tp: HKTypeLambda => caseText(tp.resultType) |
| 80 | + case defn.MatchCase(pat, body) => i"case $pat => $body" |
| 81 | + case _ => i"case $tp" |
| 82 | + |
| 83 | + private def casesText(cases: List[Type])(using Context) = |
| 84 | + i"${cases.map(caseText)}%\n %" |
| 85 | + |
| 86 | + private def explainEntry(entry: TraceEntry)(using Context): String = entry match |
| 87 | + case TryReduce(scrut: Type) => |
| 88 | + i" trying to reduce $scrut" |
| 89 | + case NoMatches(scrut, cases) => |
| 90 | + i""" failed since selector $scrut |
| 91 | + | matches none of the cases |
| 92 | + | |
| 93 | + | ${casesText(cases)}""" |
| 94 | + case Stuck(scrut, stuckCase, otherCases) => |
| 95 | + i""" failed since selector $scrut |
| 96 | + | does not match ${caseText(stuckCase)} |
| 97 | + | and cannot be shown to be disjoint from it either. |
| 98 | + | Therefore, reduction cannot advance to the remaining case${if otherCases.length == 1 then "" else "s"} |
| 99 | + | |
| 100 | + | ${casesText(otherCases)}""" |
| 101 | + |
| 102 | +end MatchTypeTrace |
| 103 | + |
0 commit comments