From 72961a8a51c0c70807c66461cc52710d2bb07313 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 8 Aug 2021 12:07:34 +0200 Subject: [PATCH 1/7] Add a phase for recomputing and rechecking all types in a typed Scala program. This is useful for two reasons: = It gives us additional explanation and validation what constitutes a well-typed Scala 3 program. Recheck has less than 300 lines of code, which is a lot less than Typer and associated files. - It can be used as a basis for phases that refine the original types with new kinds of types and new rules. --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 + .../dotty/tools/dotc/config/Printers.scala | 1 + .../tools/dotc/config/ScalaSettings.scala | 1 + .../src/dotty/tools/dotc/core/NamerOps.scala | 20 ++ .../tools/dotc/transform/PreRecheck.scala | 21 ++ .../dotty/tools/dotc/transform/Recheck.scala | 329 ++++++++++++++++++ .../tools/dotc/typer/RefineTypes.overflow | 0 compiler/test/dotc/pos-test-recheck.exludes | 7 + compiler/test/dotc/run-test-recheck.exludes | 0 compiler/test/dotty/tools/TestSources.scala | 4 + .../dotty/tools/dotc/CompilationTests.scala | 13 +- .../tools/vulpix/TestConfiguration.scala | 1 + tests/neg/i6635a.scala | 19 + tests/pos/i6635.scala | 7 +- tests/pos/i6635a.scala | 14 + 15 files changed, 434 insertions(+), 5 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/PreRecheck.scala create mode 100644 compiler/src/dotty/tools/dotc/transform/Recheck.scala create mode 100644 compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow create mode 100644 compiler/test/dotc/pos-test-recheck.exludes create mode 100644 compiler/test/dotc/run-test-recheck.exludes create mode 100644 tests/neg/i6635a.scala create mode 100644 tests/pos/i6635a.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index b2daaa4701dd..1f5302fe6198 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -101,6 +101,8 @@ class Compiler { new TupleOptimizations, // Optimize generic operations on tuples new LetOverApply, // Lift blocks from receivers of applications new ArrayConstructors) :: // Intercept creation of (non-generic) arrays and intrinsify. + List(new PreRecheck) :: + List(new TestRecheck) :: List(new Erasure) :: // Rewrite types to JVM model, erasing all type parameters, abstract types and refinements. List(new ElimErasedValueType, // Expand erased value types to their underlying implmementation types new PureStats, // Remove pure stats from blocks diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 8e13e50e59b7..b71e1e7f188a 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -38,6 +38,7 @@ object Printers { val pickling = noPrinter val quotePickling = noPrinter val plugins = noPrinter + val recheckr = noPrinter val refcheck = noPrinter val simplify = noPrinter val staging = noPrinter diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index f3a2d1f2f31f..fcd6bcb34b2e 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -238,6 +238,7 @@ private sealed trait YSettings: val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") + val Yrecheck: Setting[Boolean] = BooleanSetting("-Yrecheck", "Run type rechecks (test only)") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 9444270ccb05..a5d3e95c8f3e 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -177,4 +177,24 @@ object NamerOps: cls.registeredCompanion = modcls modcls.registeredCompanion = cls + /** For secondary constructors, make it known in the context that their type parameters + * are aliases of the class type parameters. This is done by (ab?)-using GADT constraints. + * See pos/i941.scala + */ + def linkConstructorParams(sym: Symbol)(using Context): Context = + if sym.isConstructor && !sym.isPrimaryConstructor then + sym.rawParamss match + case (tparams @ (tparam :: _)) :: _ if tparam.isType => + val rhsCtx = ctx.fresh.setFreshGADTBounds + rhsCtx.gadt.addToConstraint(tparams) + tparams.lazyZip(sym.owner.typeParams).foreach { (psym, tparam) => + val tr = tparam.typeRef + rhsCtx.gadt.addBound(psym, tr, isUpper = false) + rhsCtx.gadt.addBound(psym, tr, isUpper = true) + } + rhsCtx + case _ => + ctx + else ctx + end NamerOps diff --git a/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala new file mode 100644 index 000000000000..ab27bb3bb306 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala @@ -0,0 +1,21 @@ +package dotty.tools.dotc +package transform + +import core.Phases.Phase +import core.DenotTransformers.IdentityDenotTransformer +import core.Contexts.{Context, ctx} + +/** A phase that precedes the rechecker and that allows installing + * new types for local symbols. + */ +class PreRecheck extends Phase, IdentityDenotTransformer: + + def phaseName: String = "preRecheck" + + override def isEnabled(using Context) = next.isEnabled + + override def changesBaseTypes: Boolean = true + + def run(using Context): Unit = () + + override def isCheckable = false diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala new file mode 100644 index 000000000000..bedff8f62498 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -0,0 +1,329 @@ +package dotty.tools +package dotc +package transform + +import core.* +import Symbols.*, Contexts.*, Types.*, ContextOps.*, Decorators.*, SymDenotations.* +import Flags.*, SymUtils.*, NameKinds.* +import Phases.Phase +import DenotTransformers.IdentityDenotTransformer +import NamerOps.{methodType, linkConstructorParams} +import NullOpsDecorator.stripNull +import typer.ErrorReporting.err +import ast.* +import typer.ProtoTypes.* +import config.Printers.recheckr +import util.Property +import StdNames.nme +import reporting.trace + +abstract class Recheck extends Phase, IdentityDenotTransformer: + thisPhase => + + import ast.tpd.* + + def preRecheckPhase = this.prev.asInstanceOf[PreRecheck] + + override def isEnabled(using Context) = ctx.settings.Yrecheck.value + override def changesBaseTypes: Boolean = true + + override def isCheckable = false + // TODO: investigate what goes wrong we Ycheck directly after rechecking. + // One failing test is pos/i583a.scala + + def run(using Context): Unit = + val unit = ctx.compilationUnit + //println(i"recheck types of $unit") + newRechecker().check() + + def newRechecker()(using Context): Rechecker + + class Rechecker(ictx: Context): + val ta = ictx.typeAssigner + + extension (sym: Symbol) def updateInfo(newInfo: Type)(using Context): Unit = + if sym.info ne newInfo then + sym.copySymDenotation().installAfter(thisPhase) // reset + sym.copySymDenotation( + info = newInfo, + initFlags = + if newInfo.isInstanceOf[LazyType] then sym.flags &~ Touched + else sym.flags + ).installAfter(preRecheckPhase) + + /** Hook to be overridden */ + protected def reinfer(tp: Type)(using Context): Type = tp + + def reinferResult(info: Type)(using Context): Type = info match + case info: MethodOrPoly => + info.derivedLambdaType(resType = reinferResult(info.resultType)) + case _ => + reinfer(info) + + def enterDef(stat: Tree)(using Context): Unit = + val sym = stat.symbol + stat match + case stat: ValOrDefDef if stat.tpt.isInstanceOf[InferredTypeTree] => + sym.updateInfo(reinferResult(sym.info)) + case stat: Bind => + sym.updateInfo(reinferResult(sym.info)) + case _ => + + def recheckIdent(tree: Ident)(using Context): Type = + tree.tpe + + /** Keep the symbol of the `select` but re-infer its type */ + def recheckSelect(tree: Select)(using Context): Type = tree match + case Select(qual, name) => + val qualType = recheck(qual).widenIfUnstable + if name.is(OuterSelectName) then tree.tpe + else + //val pre = ta.maybeSkolemizePrefix(qualType, name) + val mbr = qualType.findMember(name, qualType, + excluded = if tree.symbol.is(Private) then EmptyFlags else Private + ).suchThat(tree.symbol ==) + qualType.select(name, mbr) + + def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match + case Bind(name, body) => + enterDef(tree) + val bodyType = recheck(body, pt) + val sym = tree.symbol + if sym.isType then sym.typeRef else sym.info + + def recheckLabeled(tree: Labeled, pt: Type)(using Context): Type = tree match + case Labeled(bind, expr) => + val bindType = recheck(bind, pt) + val exprType = recheck(expr, defn.UnitType) + bindType + + def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = + if !tree.rhs.isEmpty then recheck(tree.rhs, tree.symbol.info) + sym.termRef + + def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = + tree.paramss.foreach(_.foreach(enterDef)) + val rhsCtx = linkConstructorParams(sym) + if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then + recheck(tree.rhs, tree.symbol.localReturnType)(using rhsCtx) + sym.termRef + + def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type = + recheck(tree.rhs) + sym.typeRef + + def recheckClassDef(tree: TypeDef, impl: Template, sym: ClassSymbol)(using Context): Type = + recheck(impl.constr) + impl.parentsOrDerived.foreach(recheck(_)) + recheck(impl.self) + recheckStats(impl.body) + sym.typeRef + + // Need to remap Object to FromJavaObject since it got lost in ElimRepeated + private def mapJavaArgs(formals: List[Type])(using Context): List[Type] = + val tm = new TypeMap: + def apply(t: Type) = t match + case t: TypeRef if t.symbol == defn.ObjectClass => defn.FromJavaObjectType + case _ => mapOver(t) + formals.mapConserve(tm) + + def recheckApply(tree: Apply, pt: Type)(using Context): Type = + recheck(tree.fun).widen match + case fntpe: MethodType => + assert(sameLength(fntpe.paramInfos, tree.args)) + val formals = + if tree.symbol.is(JavaDefined) then mapJavaArgs(fntpe.paramInfos) + else fntpe.paramInfos + def recheckArgs(args: List[Tree], formals: List[Type], prefs: List[ParamRef]): List[Type] = args match + case arg :: args1 => + val argType = recheck(arg, formals.head) + val formals1 = + if fntpe.isParamDependent + then formals.tail.map(_.substParam(prefs.head, argType)) + else formals.tail + argType :: recheckArgs(args1, formals1, prefs.tail) + case Nil => + assert(formals.isEmpty) + Nil + val argTypes = recheckArgs(tree.args, formals, fntpe.paramRefs) + fntpe.instantiate(argTypes) + + def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = + recheck(tree.fun).widen match + case fntpe: PolyType => + assert(sameLength(fntpe.paramInfos, tree.args)) + val argTypes = tree.args.map(recheck(_)) + fntpe.instantiate(argTypes) + + def recheckTyped(tree: Typed)(using Context): Type = + val tptType = recheck(tree.tpt) + recheck(tree.expr, tptType) + tptType + + def recheckAssign(tree: Assign)(using Context): Type = + val lhsType = recheck(tree.lhs) + recheck(tree.rhs, lhsType.widen) + defn.UnitType + + def recheckBlock(stats: List[Tree], expr: Tree, pt: Type)(using Context): Type = + recheckStats(stats) + val exprType = recheck(expr, pt.dropIfProto) + TypeOps.avoid(exprType, localSyms(stats).filterConserve(_.isTerm)) + + def recheckBlock(tree: Block, pt: Type)(using Context): Type = + recheckBlock(tree.stats, tree.expr, pt) + + def recheckInlined(tree: Inlined, pt: Type)(using Context): Type = + recheckBlock(tree.bindings, tree.expansion, pt) + + def recheckIf(tree: If, pt: Type)(using Context): Type = + recheck(tree.cond, defn.BooleanType) + recheck(tree.thenp, pt) | recheck(tree.elsep, pt) + + def recheckClosure(tree: Closure, pt: Type)(using Context): Type = + if tree.tpt.isEmpty then + tree.meth.tpe.widen.toFunctionType(tree.meth.symbol.is(JavaDefined)) + else + recheck(tree.tpt) + + def recheckMatch(tree: Match, pt: Type)(using Context): Type = + val selectorType = recheck(tree.selector) + val casesTypes = tree.cases.map(recheck(_, selectorType.widen, pt)) + TypeComparer.lub(casesTypes) + + def recheck(tree: CaseDef, selType: Type, pt: Type)(using Context): Type = + recheck(tree.pat, selType) + recheck(tree.guard, defn.BooleanType) + recheck(tree.body, pt) + + def recheckReturn(tree: Return)(using Context): Type = + recheck(tree.expr, tree.from.symbol.returnProto) + defn.NothingType + + def recheckWhileDo(tree: WhileDo)(using Context): Type = + recheck(tree.cond, defn.BooleanType) + recheck(tree.body, defn.UnitType) + defn.UnitType + + def recheckTry(tree: Try, pt: Type)(using Context): Type = + val bodyType = recheck(tree.expr, pt) + val casesTypes = tree.cases.map(recheck(_, defn.ThrowableType, pt)) + val finalizerType = recheck(tree.finalizer, defn.UnitType) + TypeComparer.lub(bodyType :: casesTypes) + + def recheckSeqLiteral(tree: SeqLiteral, pt: Type)(using Context): Type = + val elemProto = pt.stripNull.elemType match + case NoType => WildcardType + case bounds: TypeBounds => WildcardType(bounds) + case elemtp => elemtp + val declaredElemType = recheck(tree.elemtpt) + val elemTypes = tree.elems.map(recheck(_, elemProto)) + TypeComparer.lub(declaredElemType :: elemTypes) + + def recheckTypeTree(tree: TypeTree)(using Context): Type = tree match + case tree: InferredTypeTree => reinfer(tree.tpe) + case _ => tree.tpe + + def recheckAnnotated(tree: Annotated)(using Context): Type = + tree.tpe match + case tp: AnnotatedType => + val argType = recheck(tree.arg) + tp.derivedAnnotatedType(argType, tp.annot) + + def recheckAlternative(tree: Alternative, pt: Type)(using Context): Type = + val altTypes = tree.trees.map(recheck(_, pt)) + TypeComparer.lub(altTypes) + + def recheckPackageDef(tree: PackageDef)(using Context): Type = + recheckStats(tree.stats) + NoType + + def recheckStats(stats: List[Tree])(using Context): Unit = + stats.foreach(enterDef) + stats.foreach(recheck(_)) + + /** Typecheck tree without adapting it, returning a recheck tree. + * @param initTree the unrecheck tree + * @param pt the expected result type + * @param locked the set of type variables of the current typer state that cannot be interpolated + * at the present time + */ + def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = trace(i"rechecking $tree, ${tree.getClass} with $pt", recheckr, show = true) { + + def recheckNamed(tree: NameTree, pt: Type)(using Context): Type = + val sym = tree.symbol + tree match + case tree: Ident => recheckIdent(tree) + case tree: Select => recheckSelect(tree) + case tree: Bind => recheckBind(tree, pt) + case tree: ValDef => + if tree.isEmpty then NoType + else recheckValDef(tree, sym)(using ctx.localContext(tree, sym)) + case tree: DefDef => + recheckDefDef(tree, sym)(using ctx.localContext(tree, sym)) + case tree: TypeDef => + tree.rhs match + case impl: Template => + recheckClassDef(tree, impl, sym.asClass)(using ctx.localContext(tree, sym)) + case _ => + recheckTypeDef(tree, sym)(using ctx.localContext(tree, sym)) + case tree: Labeled => recheckLabeled(tree, pt) + + def recheckUnnamed(tree: Tree, pt: Type): Type = tree match + case tree: Apply => recheckApply(tree, pt) + case tree: TypeApply => recheckTypeApply(tree, pt) + case _: New | _: This | _: Super | _: Literal => tree.tpe + case tree: Typed => recheckTyped(tree) + case tree: Assign => recheckAssign(tree) + case tree: Block => recheckBlock(tree, pt) + case tree: If => recheckIf(tree, pt) + case tree: Closure => recheckClosure(tree, pt) + case tree: Match => recheckMatch(tree, pt) + case tree: Return => recheckReturn(tree) + case tree: WhileDo => recheckWhileDo(tree) + case tree: Try => recheckTry(tree, pt) + case tree: SeqLiteral => recheckSeqLiteral(tree, pt) + case tree: Inlined => recheckInlined(tree, pt) + case tree: TypeTree => recheckTypeTree(tree) + case tree: Annotated => recheckAnnotated(tree) + case tree: Alternative => recheckAlternative(tree, pt) + case tree: PackageDef => recheckPackageDef(tree) + case tree: Thicket => defn.NothingType + + try + val result = tree match + case tree: NameTree => recheckNamed(tree, pt) + case tree => recheckUnnamed(tree, pt) + checkConforms(result, pt, tree) + result + catch case ex: Exception => + println(i"error while rechecking $tree") + throw ex + } + end recheck + + def checkConforms(tpe: Type, pt: Type, tree: Tree)(using Context): Unit = tree match + case _: DefTree | EmptyTree | _: TypeTree => + case _ => + val actual = tree.tpe.widenExpr + val expected = pt.widenExpr + val isCompatible = + actual <:< expected + || expected.isRepeatedParam + && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) + if !isCompatible then + err.typeMismatch(tree, pt) + + def check()(using Context): Unit = + val unit = ictx.compilationUnit + recheck(unit.tpdTree) + + end Rechecker +end Recheck + +class TestRecheck extends Recheck: + def phaseName: String = "recheck" + //override def isEnabled(using Context) = ctx.settings.YrefineTypes.value + def newRechecker()(using Context): Rechecker = Rechecker(ctx) + + diff --git a/compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow b/compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compiler/test/dotc/pos-test-recheck.exludes b/compiler/test/dotc/pos-test-recheck.exludes new file mode 100644 index 000000000000..a056090a096c --- /dev/null +++ b/compiler/test/dotc/pos-test-recheck.exludes @@ -0,0 +1,7 @@ +# Cannot compensate dealiasing due to false result dependency +i6635a.scala +i6682a.scala + +# Cannot handle closures with skolem types +i6199b.scala +i6199c.scala diff --git a/compiler/test/dotc/run-test-recheck.exludes b/compiler/test/dotc/run-test-recheck.exludes new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compiler/test/dotty/tools/TestSources.scala b/compiler/test/dotty/tools/TestSources.scala index 4fbf0e9fc5dd..c3c3937720f8 100644 --- a/compiler/test/dotty/tools/TestSources.scala +++ b/compiler/test/dotty/tools/TestSources.scala @@ -11,17 +11,21 @@ object TestSources { def posFromTastyBlacklistFile: String = "compiler/test/dotc/pos-from-tasty.blacklist" def posTestPicklingBlacklistFile: String = "compiler/test/dotc/pos-test-pickling.blacklist" + def posTestRecheckExcludesFile = "compiler/test/dotc/pos-test-recheck.exludes" def posFromTastyBlacklisted: List[String] = loadList(posFromTastyBlacklistFile) def posTestPicklingBlacklisted: List[String] = loadList(posTestPicklingBlacklistFile) + def posTestRecheckExcluded = loadList(posTestRecheckExcludesFile) // run tests lists def runFromTastyBlacklistFile: String = "compiler/test/dotc/run-from-tasty.blacklist" def runTestPicklingBlacklistFile: String = "compiler/test/dotc/run-test-pickling.blacklist" + def runTestRecheckExcludesFile = "compiler/test/dotc/run-test-recheck.exludes" def runFromTastyBlacklisted: List[String] = loadList(runFromTastyBlacklistFile) def runTestPicklingBlacklisted: List[String] = loadList(runTestPicklingBlacklistFile) + def runTestRecheckExcluded = loadList(runTestRecheckExcludesFile) // load lists diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index a88f94565e32..c4f4acc5b65a 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -220,6 +220,15 @@ class CompilationTests { ).checkCompile() } + @Test def recheck: Unit = + given TestGroup = TestGroup("recheck") + aggregateTests( + compileFilesInDir("tests/new", recheckOptions), + compileFilesInDir("tests/pos", recheckOptions, FileFilter.exclude(TestSources.posTestRecheckExcluded)), + compileFilesInDir("tests/run", recheckOptions, FileFilter.exclude(TestSources.runTestRecheckExcluded)) + ).checkCompile() + + /** The purpose of this test is three-fold, being able to compile dotty * bootstrapped, and making sure that TASTY can link against a compiled * version of Dotty, and compiling the compiler using the SemanticDB generation @@ -246,7 +255,7 @@ class CompilationTests { Properties.compilerInterface, Properties.scalaLibrary, Properties.scalaAsm, Properties.dottyInterfaces, Properties.jlineTerminal, Properties.jlineReader, ).mkString(File.pathSeparator), - Array("-Ycheck-reentrant", "-language:postfixOps", "-Xsemanticdb") + Array("-Ycheck-reentrant", "-Yrecheck", "-language:postfixOps", "-Xsemanticdb") ) val libraryDirs = List(Paths.get("library/src"), Paths.get("library/src-bootstrapped")) @@ -254,7 +263,7 @@ class CompilationTests { val lib = compileList("lib", librarySources, - defaultOptions.and("-Ycheck-reentrant", + defaultOptions.and("-Ycheck-reentrant", "-Yrecheck", "-language:experimental.erasedDefinitions", // support declaration of scala.compiletime.erasedValue // "-source", "future", // TODO: re-enable once we allow : @unchecked in pattern definitions. Right now, lots of narrowing pattern definitions fail. ))(libGroup) diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index caa7bc911633..f2e33ae8ff65 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -79,6 +79,7 @@ object TestConfiguration { ) val picklingWithCompilerOptions = picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) + val recheckOptions = defaultOptions.and("-Yrecheck") val scala2CompatMode = defaultOptions.and("-source", "3.0-migration") val explicitUTF8 = defaultOptions and ("-encoding", "UTF8") val explicitUTF16 = defaultOptions and ("-encoding", "UTF16") diff --git a/tests/neg/i6635a.scala b/tests/neg/i6635a.scala new file mode 100644 index 000000000000..a79ea4e7c818 --- /dev/null +++ b/tests/neg/i6635a.scala @@ -0,0 +1,19 @@ +object Test { + abstract class ExprBase { s => + type A + } + + abstract class Lit extends ExprBase { s => + type A = Int + val n: A + } + + // It would be nice if the following could typecheck. We'd need to apply + // a reasoning like this: + // + // Since there is an argument `e2` of type `Lit & e1.type`, it follows that + // e1.type == e2.type Hence, e1.A == e2.A == Int. This looks similar + // to techniques used in GADTs. + // + def castTestFail2a(e1: ExprBase)(e2: Lit & e1.type)(x: e1.A): Int = x // error: Found: (x : e1.A) Required: Int +} diff --git a/tests/pos/i6635.scala b/tests/pos/i6635.scala index dacd1ef5cd8b..406eee6251e6 100644 --- a/tests/pos/i6635.scala +++ b/tests/pos/i6635.scala @@ -27,11 +27,12 @@ object Test { def castTest5a(e1: ExprBase)(e2: LitU with e1.type)(x: e2.A): e1.A = x def castTest5b(e1: ExprBase)(e2: LitL with e1.type)(x: e2.A): e1.A = x - //fail: def castTestFail1(e1: ExprBase)(e2: Lit with e1.type)(x: e2.A): e1.A = x // this is like castTest5a/b, but with Lit instead of LitU/LitL - // the other direction never works: - def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x + + // The next example fails rechecking. It is repeated in i6635a.scala + // def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x def castTestFail2b(e1: ExprBase)(e2: LitL with e1.type)(x: e1.A): e2.A = x + def castTestFail2c(e1: ExprBase)(e2: LitU with e1.type)(x: e1.A): e2.A = x // the problem isn't about order of intersections. diff --git a/tests/pos/i6635a.scala b/tests/pos/i6635a.scala new file mode 100644 index 000000000000..9454e03e3a4a --- /dev/null +++ b/tests/pos/i6635a.scala @@ -0,0 +1,14 @@ +object Test { + abstract class ExprBase { s => + type A + } + + abstract class Lit extends ExprBase { s => + type A = Int + val n: A + } + + // Fails recheck since the result type e2.A is converted to Int to avoid + // a false dependency on e2. + def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x +} From 78d40def7306b1348e0d6eccbd0f5acc78782057 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 13 Aug 2021 11:09:51 +0200 Subject: [PATCH 2/7] Fix checkConforms test in Recheck --- compiler/src/dotty/tools/dotc/transform/Recheck.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index bedff8f62498..26b949e7c8aa 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -34,7 +34,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def run(using Context): Unit = val unit = ctx.compilationUnit //println(i"recheck types of $unit") - newRechecker().check() + newRechecker().checkUnit(unit) def newRechecker()(using Context): Rechecker @@ -248,7 +248,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: * @param locked the set of type variables of the current typer state that cannot be interpolated * at the present time */ - def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = trace(i"rechecking $tree, ${tree.getClass} with $pt", recheckr, show = true) { + def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = trace(i"rechecking $tree with pt = $pt", recheckr, show = true) { def recheckNamed(tree: NameTree, pt: Type)(using Context): Type = val sym = tree.symbol @@ -305,17 +305,16 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def checkConforms(tpe: Type, pt: Type, tree: Tree)(using Context): Unit = tree match case _: DefTree | EmptyTree | _: TypeTree => case _ => - val actual = tree.tpe.widenExpr + val actual = tpe.widenExpr val expected = pt.widenExpr val isCompatible = actual <:< expected || expected.isRepeatedParam && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) if !isCompatible then - err.typeMismatch(tree, pt) + err.typeMismatch(tree.withType(tpe), pt) - def check()(using Context): Unit = - val unit = ictx.compilationUnit + def checkUnit(unit: CompilationUnit)(using Context): Unit = recheck(unit.tpdTree) end Rechecker From 424c7191af3d476ce4aba8099ae8658e7ea930f1 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 13 Aug 2021 12:53:02 +0200 Subject: [PATCH 3/7] Fix rechecking of SeqLiterals --- .../dotty/tools/dotc/transform/Recheck.scala | 5 +++-- .../dotty/tools/dotc/typer/TypeAssigner.scala | 18 +++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 26b949e7c8aa..83f05c7e82b9 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -5,13 +5,14 @@ package transform import core.* import Symbols.*, Contexts.*, Types.*, ContextOps.*, Decorators.*, SymDenotations.* import Flags.*, SymUtils.*, NameKinds.* +import ast.* import Phases.Phase import DenotTransformers.IdentityDenotTransformer import NamerOps.{methodType, linkConstructorParams} import NullOpsDecorator.stripNull import typer.ErrorReporting.err -import ast.* import typer.ProtoTypes.* +import typer.TypeAssigner.seqLitType import config.Printers.recheckr import util.Property import StdNames.nme @@ -218,7 +219,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: case elemtp => elemtp val declaredElemType = recheck(tree.elemtpt) val elemTypes = tree.elems.map(recheck(_, elemProto)) - TypeComparer.lub(declaredElemType :: elemTypes) + seqLitType(tree, TypeComparer.lub(declaredElemType :: elemTypes)) def recheckTypeTree(tree: TypeTree)(using Context): Type = tree match case tree: InferredTypeTree => reinfer(tree.tpe) diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index f7e33bd4a5f7..2a5a9ca284ac 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -17,7 +17,8 @@ import reporting._ import Checking.{checkNoPrivateLeaks, checkNoWildcard} trait TypeAssigner { - import tpd._ + import tpd.* + import TypeAssigner.* /** The qualifying class of a this or super with prefix `qual` (which might be empty). * @param packageOk The qualifier may refer to a package. @@ -435,13 +436,8 @@ trait TypeAssigner { if (cases.isEmpty) tree.withType(expr.tpe) else tree.withType(TypeComparer.lub(expr.tpe :: cases.tpes)) - def assignType(tree: untpd.SeqLiteral, elems: List[Tree], elemtpt: Tree)(using Context): SeqLiteral = { - val ownType = tree match { - case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemtpt.tpe) - case _ => if (ctx.erasedTypes) defn.SeqType else defn.SeqType.appliedTo(elemtpt.tpe) - } - tree.withType(ownType) - } + def assignType(tree: untpd.SeqLiteral, elems: List[Tree], elemtpt: Tree)(using Context): SeqLiteral = + tree.withType(seqLitType(tree, elemtpt.tpe)) def assignType(tree: untpd.SingletonTypeTree, ref: Tree)(using Context): SingletonTypeTree = tree.withType(ref.tpe) @@ -527,5 +523,9 @@ trait TypeAssigner { tree.withType(pid.symbol.termRef) } +object TypeAssigner extends TypeAssigner: + def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match + case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType) + case _ => if ctx.erasedTypes then defn.SeqType else defn.SeqType.appliedTo(elemType) + -object TypeAssigner extends TypeAssigner From 749000167345f03dea5f7dd8ee69e7f58cdba6a5 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 13 Aug 2021 12:55:00 +0200 Subject: [PATCH 4/7] Re-constant-fold when rechecking This is necessary since not all constant folded terms are converted to literals. The conversion does not happen if one of the operands is impure. A test case is run/final-field.scala --- .../src/dotty/tools/dotc/transform/Recheck.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 83f05c7e82b9..0790882fa399 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -13,6 +13,7 @@ import NullOpsDecorator.stripNull import typer.ErrorReporting.err import typer.ProtoTypes.* import typer.TypeAssigner.seqLitType +import typer.ConstFold import config.Printers.recheckr import util.Property import StdNames.nme @@ -70,6 +71,11 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: sym.updateInfo(reinferResult(sym.info)) case _ => + def constFold(tree: Tree, tp: Type)(using Context): Type = + val tree1 = tree.withType(tp) + val tree2 = ConstFold(tree1) + if tree2 ne tree1 then tree2.tpe else tp + def recheckIdent(tree: Ident)(using Context): Type = tree.tpe @@ -83,7 +89,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: val mbr = qualType.findMember(name, qualType, excluded = if tree.symbol.is(Private) then EmptyFlags else Private ).suchThat(tree.symbol ==) - qualType.select(name, mbr) + constFold(tree, qualType.select(name, mbr)) def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match case Bind(name, body) => @@ -147,14 +153,14 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: assert(formals.isEmpty) Nil val argTypes = recheckArgs(tree.args, formals, fntpe.paramRefs) - fntpe.instantiate(argTypes) + constFold(tree, fntpe.instantiate(argTypes)) def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = recheck(tree.fun).widen match case fntpe: PolyType => assert(sameLength(fntpe.paramInfos, tree.args)) val argTypes = tree.args.map(recheck(_)) - fntpe.instantiate(argTypes) + constFold(tree, fntpe.instantiate(argTypes)) def recheckTyped(tree: Typed)(using Context): Type = val tptType = recheck(tree.tpt) From 8afe649e40eaec450d014445aff41566b4eeb93e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 13 Aug 2021 12:55:31 +0200 Subject: [PATCH 5/7] Another rechecking exclude Detected when fixing the conforms check --- compiler/test/dotc/pos-test-recheck.exludes | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/test/dotc/pos-test-recheck.exludes b/compiler/test/dotc/pos-test-recheck.exludes index a056090a096c..ea1ddd15b12c 100644 --- a/compiler/test/dotc/pos-test-recheck.exludes +++ b/compiler/test/dotc/pos-test-recheck.exludes @@ -5,3 +5,4 @@ i6682a.scala # Cannot handle closures with skolem types i6199b.scala i6199c.scala +i11247.scala From 2b88f1cb2bd59308c2e7944c712e97237899289d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 13 Aug 2021 13:13:31 +0200 Subject: [PATCH 6/7] Address review comments - rename `exludes` --> `excludes` - fix comments --- .../src/dotty/tools/dotc/transform/Recheck.scala | 12 ++++-------- ...est-recheck.exludes => pos-test-recheck.excludes} | 0 ...est-recheck.exludes => run-test-recheck.excludes} | 0 compiler/test/dotty/tools/TestSources.scala | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) rename compiler/test/dotc/{pos-test-recheck.exludes => pos-test-recheck.excludes} (100%) rename compiler/test/dotc/{run-test-recheck.exludes => run-test-recheck.excludes} (100%) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 0790882fa399..d2d1a39ad0b9 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -34,9 +34,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: // One failing test is pos/i583a.scala def run(using Context): Unit = - val unit = ctx.compilationUnit - //println(i"recheck types of $unit") - newRechecker().checkUnit(unit) + newRechecker().checkUnit(ctx.compilationUnit) def newRechecker()(using Context): Rechecker @@ -94,7 +92,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match case Bind(name, body) => enterDef(tree) - val bodyType = recheck(body, pt) + recheck(body, pt) val sym = tree.symbol if sym.isType then sym.typeRef else sym.info @@ -249,11 +247,9 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: stats.foreach(enterDef) stats.foreach(recheck(_)) - /** Typecheck tree without adapting it, returning a recheck tree. - * @param initTree the unrecheck tree + /** Recheck tree without adapting it, returning its new type. + * @param tree the original tree * @param pt the expected result type - * @param locked the set of type variables of the current typer state that cannot be interpolated - * at the present time */ def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = trace(i"rechecking $tree with pt = $pt", recheckr, show = true) { diff --git a/compiler/test/dotc/pos-test-recheck.exludes b/compiler/test/dotc/pos-test-recheck.excludes similarity index 100% rename from compiler/test/dotc/pos-test-recheck.exludes rename to compiler/test/dotc/pos-test-recheck.excludes diff --git a/compiler/test/dotc/run-test-recheck.exludes b/compiler/test/dotc/run-test-recheck.excludes similarity index 100% rename from compiler/test/dotc/run-test-recheck.exludes rename to compiler/test/dotc/run-test-recheck.excludes diff --git a/compiler/test/dotty/tools/TestSources.scala b/compiler/test/dotty/tools/TestSources.scala index c3c3937720f8..60070bb15af3 100644 --- a/compiler/test/dotty/tools/TestSources.scala +++ b/compiler/test/dotty/tools/TestSources.scala @@ -11,7 +11,7 @@ object TestSources { def posFromTastyBlacklistFile: String = "compiler/test/dotc/pos-from-tasty.blacklist" def posTestPicklingBlacklistFile: String = "compiler/test/dotc/pos-test-pickling.blacklist" - def posTestRecheckExcludesFile = "compiler/test/dotc/pos-test-recheck.exludes" + def posTestRecheckExcludesFile = "compiler/test/dotc/pos-test-recheck.excludes" def posFromTastyBlacklisted: List[String] = loadList(posFromTastyBlacklistFile) def posTestPicklingBlacklisted: List[String] = loadList(posTestPicklingBlacklistFile) @@ -21,7 +21,7 @@ object TestSources { def runFromTastyBlacklistFile: String = "compiler/test/dotc/run-from-tasty.blacklist" def runTestPicklingBlacklistFile: String = "compiler/test/dotc/run-test-pickling.blacklist" - def runTestRecheckExcludesFile = "compiler/test/dotc/run-test-recheck.exludes" + def runTestRecheckExcludesFile = "compiler/test/dotc/run-test-recheck.excludes" def runFromTastyBlacklisted: List[String] = loadList(runFromTastyBlacklistFile) def runTestPicklingBlacklisted: List[String] = loadList(runTestPicklingBlacklistFile) From 03a7bd8f6b23a884d304e5002956732ece908c08 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 13 Aug 2021 13:55:23 +0200 Subject: [PATCH 7/7] Loosen skolem comparisons when rechecking Treat a SolemType(T) as a supertype (as well as a subtype) of T. --- compiler/src/dotty/tools/dotc/core/Phases.scala | 3 +++ compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 2 ++ compiler/src/dotty/tools/dotc/transform/Recheck.scala | 3 +++ compiler/test/dotc/pos-test-recheck.excludes | 5 ----- compiler/test/dotty/tools/dotc/CompilationTests.scala | 8 +++++--- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index b1268d7034a9..0294de01f36e 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -295,6 +295,9 @@ object Phases { /** If set, implicit search is enabled */ def allowsImplicitSearch: Boolean = false + /** If set equate Skolem types with underlying types */ + def widenSkolems: Boolean = false + /** List of names of phases that should precede this phase */ def runsAfter: Set[String] = Set.empty diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index eee0e802528c..5774f750ce44 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -745,6 +745,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } compareClassInfo + case tp2: SkolemType => + ctx.phase.widenSkolems && recur(tp1, tp2.info) || fourthTry case _ => fourthTry } diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index d2d1a39ad0b9..76f89cb65757 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -33,6 +33,8 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: // TODO: investigate what goes wrong we Ycheck directly after rechecking. // One failing test is pos/i583a.scala + override def widenSkolems = true + def run(using Context): Unit = newRechecker().checkUnit(ctx.compilationUnit) @@ -315,6 +317,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: || expected.isRepeatedParam && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) if !isCompatible then + println(i"err at ${ctx.phase}") err.typeMismatch(tree.withType(tpe), pt) def checkUnit(unit: CompilationUnit)(using Context): Unit = diff --git a/compiler/test/dotc/pos-test-recheck.excludes b/compiler/test/dotc/pos-test-recheck.excludes index ea1ddd15b12c..e973b2cd529f 100644 --- a/compiler/test/dotc/pos-test-recheck.excludes +++ b/compiler/test/dotc/pos-test-recheck.excludes @@ -1,8 +1,3 @@ # Cannot compensate dealiasing due to false result dependency i6635a.scala i6682a.scala - -# Cannot handle closures with skolem types -i6199b.scala -i6199c.scala -i11247.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index c4f4acc5b65a..f2f93ad9165d 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -229,9 +229,11 @@ class CompilationTests { ).checkCompile() - /** The purpose of this test is three-fold, being able to compile dotty - * bootstrapped, and making sure that TASTY can link against a compiled - * version of Dotty, and compiling the compiler using the SemanticDB generation + /** This test serves several purposes: + * - being able to compile dotty bootstrapped, + * - making sure that TASTY can link against a compiled version of Dotty, + * - compiling the compiler using the SemanticDB generation + * - compiling the compiler under -Yrecheck mode. */ @Test def tastyBootstrap: Unit = { implicit val testGroup: TestGroup = TestGroup("tastyBootstrap/tests")