@@ -707,6 +707,88 @@ trait Checking {
707
707
checkNoForwardDependencies(vparams1)
708
708
case Nil =>
709
709
}
710
+
711
+ /** * Check that none of the type parameters of a class are part of an expansive cycle.
712
+ *
713
+ * For the definition of expansive cycles, see Kennedy, Andrew J., and Benjamin C. Pierce.
714
+ * "On decidability of nominal subtyping with variance." (2006).
715
+ */
716
+ def checkExpansiveCycles (classDef : TypeDef )(implicit ctx : Context ): Unit = {
717
+ classDef.denot.info match {
718
+ case cinfo : ClassInfo =>
719
+ cinfo.typeParams foreach {
720
+ case tparam : Symbol =>
721
+ if (expansiveParams(tparam) contains tparam) {
722
+ ctx.error(TypeParamInExpansiveCycle (cinfo, tparam), tparam.pos)
723
+ }
724
+ case _ =>
725
+ }
726
+ case _ =>
727
+ }
728
+ }
729
+
730
+ /** * A state in the type parameter dependency graph indicates the referenced type parameter and whether we got
731
+ * to it through a path that includes at least one expansive edge.
732
+ */
733
+ private case class ExpansiveSt (tparam : Symbol , expansive : Boolean )
734
+
735
+ /** * Calculates the set of type parameters that can be reached from the given type parameter by following a
736
+ * path containing at least one expansive edge. The set is calculated via BFS.
737
+ */
738
+ private def expansiveParams (tparam : Symbol )(implicit ctx : Context ): Set [Symbol ] = {
739
+ val worklist = mutable.Queue [ExpansiveSt ]()
740
+ val seen = mutable.Set [ExpansiveSt ]()
741
+ def enqueue (st : ExpansiveSt ): Unit = {
742
+ if (! (seen contains st)) {
743
+ worklist += st
744
+ seen += st
745
+ }
746
+ }
747
+ enqueue(ExpansiveSt (tparam, expansive= false ))
748
+ while (worklist.nonEmpty) {
749
+ val st @ ExpansiveSt (fromParam, expansive) = worklist.dequeue()
750
+ val findNext = new ExpansiveStAcc (st)
751
+ fromParam.owner.info match {
752
+ case cinfo : ClassInfo =>
753
+ for {
754
+ parent <- cinfo.parentsWithArgs
755
+ toParams = findNext(List .empty[ExpansiveSt ], parent)
756
+ toParam <- toParams
757
+ } enqueue(toParam)
758
+ case _ =>
759
+ }
760
+ }
761
+ Set .empty[Symbol ] ++ seen.filter((st) => st.expansive).map((st) => st.tparam)
762
+ }
763
+
764
+ /** * Given a type parameter and a type, finds all refinements in the type that reference the type parameter,
765
+ * and whether they do so in an expansive manner.
766
+ */
767
+ private class ExpansiveStAcc (state : ExpansiveSt )(implicit ctx : Context ) extends TypeAccumulator [List [ExpansiveSt ]] {
768
+ val ExpansiveSt (tparam, expansive) = state
769
+
770
+ /** Does the type reference the type parameter somewhere in it? */
771
+ def hasReference (typ : Type ): Boolean = {
772
+ typ.namedPartsWith(_.symbol == tparam).nonEmpty
773
+ }
774
+
775
+ override def apply (acc : List [ExpansiveSt ], tp : Type ): List [ExpansiveSt ] = {
776
+ tp match {
777
+ case RefinedType (parent, name : TypeName , info) =>
778
+ val toParam = parent.typeParamNamed(name)
779
+ if (info.typeSymbol == tparam) {
780
+ // A non-expansive edge: `tparam` replaces `toParam` directly.
781
+ ExpansiveSt (toParam, expansive = expansive) :: acc
782
+ } else if (hasReference(info)) {
783
+ // An expansive edge: `tparam` is passed as *part* of the argument replacing `toParam`.
784
+ this (ExpansiveSt (toParam, expansive = true ) :: acc, info)
785
+ } else {
786
+ acc
787
+ }
788
+ case _ => foldOver(acc, tp)
789
+ }
790
+ }
791
+ }
710
792
}
711
793
712
794
trait NoChecking extends Checking {
0 commit comments