Skip to content

Commit bb3656d

Browse files
committed
Introduce best-effort compilation for IDEs
2 new experimental options are introduces for the compiler: `-Ybest-effort` and `-Ywith-best-effort-tasty`. A related Best Effort TASTy (.betasty) format, a TASTy aligned file format able to hold some errored trees was also added. Behaviour of the options and the format is documented as part of this commit in the `best-effort-compilation.md` docs file. `-Ybest-effort` is used to produce `.betasty` files in the `<output>/META-INF/best-effort`. `-Ywith-best-effort-tasty` allows to use them during compilation, limiting it to the frontend phases if such file is used. If any .betasty is used, transparent inline macros also cease to be expanded by the compiler. Since best-effort compilation can fail (e.g. due to cyclic reference errors which sometimes are not able to be pickled or unpickled), the crashes caused by it are wrapped into an additional descriptive error message in the aim to fail more gracefully (and not pollute our issue tracker with known problems). The feature is tested in two ways: * with a set of pairs of dependent projects, one of which is meant to produce .betasty by using `-Ybest-effort`, and the other tries to consume it using `-Ywith-best-effort-tasty`. * by reusing the compiler nonbootstrapped neg tests, first by running them with `-Ybest-effort` option, and then by running read-tasty tests on the produced betasty files to thest best-effort tastt unpickling Additionally, `-Ywith-best-effort-tasty` allows to print `.betasty` via `-print-tasty`.
1 parent d148973 commit bb3656d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1046
-220
lines changed

compiler/src/dotty/tools/backend/jvm/GenBCode.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class GenBCode extends Phase { self =>
1717

1818
override def description: String = GenBCode.description
1919

20+
override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty
21+
2022
private val superCallsMap = new MutableSymbolMap[Set[ClassSymbol]]
2123
def registerSuperCall(sym: Symbol, calls: ClassSymbol): Unit = {
2224
val old = superCallsMap.getOrElse(sym, Set.empty)

compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class GenSJSIR extends Phase {
1212
override def description: String = GenSJSIR.description
1313

1414
override def isRunnable(using Context): Boolean =
15-
super.isRunnable && ctx.settings.scalajs.value
15+
super.isRunnable && ctx.settings.scalajs.value && !ctx.usesBestEffortTasty
1616

1717
def run(using Context): Unit =
1818
new JSCodeGen().run()

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

Lines changed: 4 additions & 2 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 read.")
4244
case ex: TypeError if !runOrNull.enrichedErrorMessage =>
4345
println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}"))
4446
throw ex
@@ -102,8 +104,8 @@ class Driver {
102104
None
103105
else file.ext match
104106
case FileExtension.Jar => Some(file.path)
105-
case FileExtension.Tasty =>
106-
TastyFileUtil.getClassPath(file) match
107+
case FileExtension.Tasty | FileExtension.Betasty =>
108+
TastyFileUtil.getClassPath(file, ctx.withBestEffortTasty) match
107109
case Some(classpath) => Some(classpath)
108110
case _ =>
109111
report.error(em"Could not load classname from: ${file.path}")

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
301301
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
302302
ctx.base.usePhases(phases, runCtx)
303303

304+
var forceReachPhaseMaybe =
305+
if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer")
306+
else None
307+
304308
if ctx.settings.YnoDoubleBindings.value then
305309
ctx.base.checkNoDoubleBindings = true
306310

@@ -311,7 +315,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
311315

312316
for phase <- allPhases do
313317
doEnterPhase(phase)
314-
val phaseWillRun = phase.isRunnable
318+
val phaseWillRun = phase.isRunnable || forceReachPhaseMaybe.nonEmpty
315319
if phaseWillRun then
316320
Stats.trackTime(s"phase time ms/$phase") {
317321
val start = System.currentTimeMillis
@@ -324,6 +328,13 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
324328
def printCtx(unit: CompilationUnit) = phase.printingContext(
325329
ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
326330
lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit))
331+
332+
forceReachPhaseMaybe match {
333+
case Some(forceReachPhase) if phase.phaseName == forceReachPhase =>
334+
forceReachPhaseMaybe = None
335+
case _ =>
336+
}
337+
327338
report.informTime(s"$phase ", start)
328339
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
329340
for (unit <- units)

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -919,12 +919,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
919919
else cpy.PackageDef(tree)(pid, slicedStats) :: Nil
920920
case tdef: TypeDef =>
921921
val sym = tdef.symbol
922-
assert(sym.isClass)
922+
if !ctx.isBestEffort then assert(sym.isClass)
923923
if (cls == sym || cls == sym.linkedClass) tdef :: Nil
924924
else Nil
925925
case vdef: ValDef =>
926926
val sym = vdef.symbol
927-
assert(sym.is(Module))
927+
if !ctx.isBestEffort then assert(sym.is(Module))
928928
if (cls == sym.companionClass || cls == sym.moduleClass) vdef :: Nil
929929
else Nil
930930
case tree =>

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
4747
case _: RefTree | _: GenericApply | _: Inlined | _: Hole =>
4848
ta.assignType(untpd.Apply(fn, args), fn, args)
4949
case _ =>
50-
assert(ctx.reporter.errorsReported)
50+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported)
5151
ta.assignType(untpd.Apply(fn, args), fn, args)
5252

5353
def TypeApply(fn: Tree, args: List[Tree])(using Context): TypeApply = fn match
@@ -56,7 +56,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
5656
case _: RefTree | _: GenericApply =>
5757
ta.assignType(untpd.TypeApply(fn, args), fn, args)
5858
case _ =>
59-
assert(ctx.reporter.errorsReported, s"unexpected tree for type application: $fn")
59+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported, s"unexpected tree for type application: $fn")
6060
ta.assignType(untpd.TypeApply(fn, args), fn, args)
6161

6262
def Literal(const: Constant)(using Context): Literal =

compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[BinaryFil
285285
protected def createFileEntry(file: AbstractFile): BinaryFileEntry = BinaryFileEntry(file)
286286

287287
protected def isMatchingFile(f: JFile): Boolean =
288-
f.isTasty || (f.isClass && !f.hasSiblingTasty)
288+
f.isTasty || f.isBestEffortTasty || (f.isClass && f.hasSiblingTasty)
289289

290290
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
291291
}

compiler/src/dotty/tools/dotc/classpath/FileUtils.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ object FileUtils {
2323

2424
def hasTastyExtension: Boolean = file.ext.isTasty
2525

26+
def hasBetastyExtension: Boolean = file.ext.isBetasty
27+
2628
def isTasty: Boolean = !file.isDirectory && hasTastyExtension
2729

30+
def isBestEffortTasty: Boolean = !file.isDirectory && hasBetastyExtension
31+
2832
def isScalaBinary: Boolean = file.isClass || file.isTasty
2933

3034
def isScalaOrJavaSource: Boolean = !file.isDirectory && file.ext.isScalaOrJava
@@ -55,6 +59,9 @@ object FileUtils {
5559

5660
def isTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_TASTY)
5761

62+
def isBestEffortTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_BETASTY)
63+
64+
5865
/**
5966
* Returns if there is an existing sibling `.tasty` file.
6067
*/
@@ -69,6 +76,7 @@ object FileUtils {
6976
private val SUFFIX_CLASS = ".class"
7077
private val SUFFIX_SCALA = ".scala"
7178
private val SUFFIX_TASTY = ".tasty"
79+
private val SUFFIX_BETASTY = ".betasty"
7280
private val SUFFIX_JAVA = ".java"
7381
private val SUFFIX_SIG = ".sig"
7482

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,9 @@ private sealed trait YSettings:
414414
val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting(ForkSetting, "Yprofile-run-gc", "Run a GC between phases - this allows heap size to be accurate at the expense of more time. Specify a list of phases, or *", "_")
415415
//.withPostSetHook( _ => YprofileEnabled.value = true )
416416

417+
val YbestEffort: Setting[Boolean] = BooleanSetting(ForkSetting, "Ybest-effort", "Enable best-effort compilation attempting to produce betasty to the META-INF/best-effort directory, regardless of errors, as part of the pickler phase.")
418+
val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Ywith-best-effort-tasty", "Allow to compile using best-effort tasty files. If such file is used, the compiler will stop after the pickler phase.")
419+
417420
// Experimental language features
418421
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism.")
419422
val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,18 @@ object Contexts {
474474

475475
/** Is the flexible types option set? */
476476
def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value
477+
478+
/** Is best-effort-dir option set? */
479+
def isBestEffort: Boolean = base.settings.YbestEffort.value
480+
481+
/** Is the from-best-effort-tasty option set to true? */
482+
def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value
483+
484+
/** Were any best effort tasty dependencies used during compilation? */
485+
def usesBestEffortTasty: Boolean = base.usedBestEffortTasty
486+
487+
/** Confirm that a best effort tasty dependency was used during compilation. */
488+
def setUsesBestEffortTasty(): Unit = base.usedBestEffortTasty = true
477489

478490
/** A fresh clone of this context embedded in this context. */
479491
def fresh: FreshContext = freshOver(this)
@@ -959,6 +971,9 @@ object Contexts {
959971
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
960972
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
961973

974+
/** Was best effort file used during compilation? */
975+
private[core] var usedBestEffortTasty = false
976+
962977
// Types state
963978
/** A table for hash consing unique types */
964979
private[core] val uniques: Uniques = Uniques()

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ object DenotTransformers {
2828

2929
/** The transformation method */
3030
def transform(ref: SingleDenotation)(using Context): SingleDenotation
31+
32+
override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty
3133
}
3234

3335
/** A transformer that only transforms the info field of denotations */

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,8 @@ object Denotations {
719719
ctx.runId >= validFor.runId
720720
|| ctx.settings.YtestPickler.value // mixing test pickler with debug printing can travel back in time
721721
|| ctx.mode.is(Mode.Printing) // no use to be picky when printing error messages
722-
|| symbol.isOneOf(ValidForeverFlags),
722+
|| symbol.isOneOf(ValidForeverFlags)
723+
|| ctx.isBestEffort || ctx.usesBestEffortTasty,
723724
s"denotation $this invalid in run ${ctx.runId}. ValidFor: $validFor")
724725
var d: SingleDenotation = this
725726
while ({

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -720,12 +720,16 @@ object SymDenotations {
720720
* TODO: Find a more robust way to characterize self symbols, maybe by
721721
* spending a Flag on them?
722722
*/
723-
final def isSelfSym(using Context): Boolean = owner.infoOrCompleter match {
724-
case ClassInfo(_, _, _, _, selfInfo) =>
725-
selfInfo == symbol ||
726-
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
727-
case _ => false
728-
}
723+
final def isSelfSym(using Context): Boolean =
724+
if !ctx.isBestEffort || exists then
725+
owner.infoOrCompleter match {
726+
case ClassInfo(_, _, _, _, selfInfo) =>
727+
selfInfo == symbol ||
728+
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
729+
case _ => false
730+
}
731+
else false
732+
729733

730734
/** Is this definition contained in `boundary`?
731735
* Same as `ownersIterator contains boundary` but more efficient.
@@ -2003,7 +2007,7 @@ object SymDenotations {
20032007
case p :: parents1 =>
20042008
p.classSymbol match {
20052009
case pcls: ClassSymbol => builder.addAll(pcls.baseClasses)
2006-
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive), s"$this has non-class parent: $p")
2010+
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty, s"$this has non-class parent: $p")
20072011
}
20082012
traverse(parents1)
20092013
case nil =>

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import java.nio.channels.ClosedByInterruptException
77

88
import scala.util.control.NonFatal
99

10-
import dotty.tools.dotc.classpath.FileUtils.hasTastyExtension
10+
import dotty.tools.dotc.classpath.FileUtils.{hasTastyExtension, hasBetastyExtension}
1111
import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile }
1212
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
1313

@@ -26,6 +26,7 @@ import parsing.JavaParsers.OutlineJavaParser
2626
import parsing.Parsers.OutlineParser
2727
import dotty.tools.tasty.{TastyHeaderUnpickler, UnpickleException, UnpicklerConfig, TastyVersion}
2828
import dotty.tools.dotc.core.tasty.TastyUnpickler
29+
import dotty.tools.tasty.besteffort.BestEffortTastyHeaderUnpickler
2930

3031
object SymbolLoaders {
3132
import ast.untpd.*
@@ -198,7 +199,7 @@ object SymbolLoaders {
198199
enterToplevelsFromSource(owner, nameOf(classRep), src)
199200
case (Some(bin), _) =>
200201
val completer =
201-
if bin.hasTastyExtension then ctx.platform.newTastyLoader(bin)
202+
if bin.hasTastyExtension || bin.hasBetastyExtension then ctx.platform.newTastyLoader(bin)
202203
else ctx.platform.newClassLoader(bin)
203204
enterClassAndModule(owner, nameOf(classRep), completer)
204205
}
@@ -261,7 +262,8 @@ object SymbolLoaders {
261262
(idx + str.TOPLEVEL_SUFFIX.length + 1 != name.length || !name.endsWith(str.TOPLEVEL_SUFFIX))
262263
}
263264

264-
def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.last == '$'
265+
def maybeModuleClass(classRep: ClassRepresentation): Boolean =
266+
classRep.name.nonEmpty && classRep.name.last == '$'
265267

266268
private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = {
267269
def isAbsent(classRep: ClassRepresentation) =
@@ -424,26 +426,39 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
424426

425427
val compilationUnitInfo: CompilationUnitInfo | Null = unpickler.compilationUnitInfo
426428

427-
def description(using Context): String = "TASTy file " + tastyFile.toString
429+
val isBestEffortTasty = tastyFile.name.endsWith(".betasty")
430+
431+
def description(using Context): String =
432+
if tastyFile.extension == ".betasty" then "Best Effort TASTy file " + tastyFile.toString
433+
else "TASTy file " + tastyFile.toString
428434

429435
override def doComplete(root: SymDenotation)(using Context): Unit =
430436
handleUnpicklingExceptions:
431-
checkTastyUUID()
432437
val (classRoot, moduleRoot) = rootDenots(root.asClass)
433-
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
434-
if mayLoadTreesFromTasty then
435-
classRoot.classSymbol.rootTreeOrProvider = unpickler
436-
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
437-
438+
if (!isBestEffortTasty || ctx.withBestEffortTasty) then
439+
val tastyBytes = tastyFile.toByteArray
440+
val unpickler = new tasty.DottyUnpickler(tastyFile, tastyBytes, isBestEffortTasty = isBestEffortTasty)
441+
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
442+
if mayLoadTreesFromTasty || (isBestEffortTasty && ctx.withBestEffortTasty) then
443+
classRoot.classSymbol.rootTreeOrProvider = unpickler
444+
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
445+
if isBestEffortTasty then
446+
checkBeTastyUUID(tastyFile, tastyBytes)
447+
ctx.setUsesBestEffortTasty()
448+
else
449+
checkTastyUUID()
450+
else
451+
report.error(em"Best Effort TASTy $tastyFile file could not be read.")
438452
private def handleUnpicklingExceptions[T](thunk: =>T): T =
439453
try thunk
440454
catch case e: RuntimeException =>
455+
val tastyType = if (isBestEffortTasty) "Best Effort TASTy" else "TASTy"
441456
val message = e match
442457
case e: UnpickleException =>
443-
s"""TASTy file ${tastyFile.canonicalPath} could not be read, failing with:
458+
s"""$tastyType file ${tastyFile.canonicalPath} could not be read, failing with:
444459
| ${Option(e.getMessage).getOrElse("")}""".stripMargin
445460
case _ =>
446-
s"""TASTy file ${tastyFile.canonicalPath} is broken, reading aborted with ${e.getClass}
461+
s"""$tastyFile file ${tastyFile.canonicalPath} is broken, reading aborted with ${e.getClass}
447462
| ${Option(e.getMessage).getOrElse("")}""".stripMargin
448463
throw IOException(message, e)
449464

@@ -460,6 +475,9 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
460475
// tasty file compiled by `-Yearly-tasty-output-write` comes from an early output jar.
461476
report.inform(s"No classfiles found for $tastyFile when checking TASTy UUID")
462477

478+
private def checkBeTastyUUID(tastyFile: AbstractFile, tastyBytes: Array[Byte])(using Context): Unit =
479+
new BestEffortTastyHeaderUnpickler(tastyBytes).readHeader()
480+
463481
private def mayLoadTreesFromTasty(using Context): Boolean =
464482
ctx.settings.YretainTrees.value || ctx.settings.fromTasty.value
465483
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,8 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
756756
MissingType(tycon.prefix, tycon.name)
757757
case _ =>
758758
TypeError(em"Cannot resolve reference to $tp")
759-
throw typeErr
759+
if ctx.isBestEffort then report.error(typeErr.toMessage)
760+
else throw typeErr
760761
tp1
761762

762763
/** Widen term ref, skipping any `()` parameter of an eventual getter. Used to erase a TermRef.

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3149,7 +3149,8 @@ object Types extends TypeUtils {
31493149
if (ctx.erasedTypes) tref
31503150
else cls.info match {
31513151
case cinfo: ClassInfo => cinfo.selfType
3152-
case _: ErrorType | NoType if ctx.mode.is(Mode.Interactive) => cls.info
3152+
case _: ErrorType | NoType
3153+
if ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty => cls.info
31533154
// can happen in IDE if `cls` is stale
31543155
}
31553156

@@ -3719,8 +3720,8 @@ object Types extends TypeUtils {
37193720

37203721
def apply(tp1: Type, tp2: Type, soft: Boolean)(using Context): OrType = {
37213722
def where = i"in union $tp1 | $tp2"
3722-
expectValueTypeOrWildcard(tp1, where)
3723-
expectValueTypeOrWildcard(tp2, where)
3723+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp1, where)
3724+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp2, where)
37243725
assertUnerased()
37253726
unique(new CachedOrType(tp1, tp2, soft))
37263727
}

0 commit comments

Comments
 (0)