mirror of
https://github.com/fluencelabs/aqua.git
synced 2024-12-12 09:45:32 +00:00
feat(compiler): Find and display link cycles (#787)
Find and display dependency cycles
This commit is contained in:
parent
8ba7021cd4
commit
667a8255d9
@ -11,7 +11,7 @@ import aqua.res.AquaRes
|
|||||||
import aqua.semantics.{CompilerState, Semantics}
|
import aqua.semantics.{CompilerState, Semantics}
|
||||||
import aqua.semantics.header.{HeaderHandler, HeaderSem, Picker}
|
import aqua.semantics.header.{HeaderHandler, HeaderSem, Picker}
|
||||||
import cats.data.*
|
import cats.data.*
|
||||||
import cats.data.Validated.{Invalid, Valid, validNec}
|
import cats.data.Validated.{validNec, Invalid, Valid}
|
||||||
import cats.parse.Parser0
|
import cats.parse.Parser0
|
||||||
import cats.syntax.applicative.*
|
import cats.syntax.applicative.*
|
||||||
import cats.syntax.flatMap.*
|
import cats.syntax.flatMap.*
|
||||||
@ -19,7 +19,7 @@ import cats.syntax.functor.*
|
|||||||
import cats.syntax.monoid.*
|
import cats.syntax.monoid.*
|
||||||
import cats.syntax.traverse.*
|
import cats.syntax.traverse.*
|
||||||
import cats.syntax.semigroup.*
|
import cats.syntax.semigroup.*
|
||||||
import cats.{Comonad, Functor, Monad, Monoid, Order, ~>}
|
import cats.{~>, Comonad, Functor, Monad, Monoid, Order}
|
||||||
import scribe.Logging
|
import scribe.Logging
|
||||||
|
|
||||||
class AquaCompiler[F[_]: Monad, E, I: Order, S[_]: Comonad, C: Monoid: Picker](
|
class AquaCompiler[F[_]: Monad, E, I: Order, S[_]: Comonad, C: Monoid: Picker](
|
||||||
@ -39,7 +39,7 @@ class AquaCompiler[F[_]: Monad, E, I: Order, S[_]: Comonad, C: Monoid: Picker](
|
|||||||
Err,
|
Err,
|
||||||
ValidatedCtxT
|
ValidatedCtxT
|
||||||
],
|
],
|
||||||
cycleError: List[AquaModule[I, Err, ValidatedCtxT]] => Err
|
cycleError: Linker.DepCycle[AquaModule[I, Err, ValidatedCtxT]] => Err
|
||||||
): ValidatedNec[Err, Map[I, ValidatedCtx]] = {
|
): ValidatedNec[Err, Map[I, ValidatedCtx]] = {
|
||||||
logger.trace("linking modules...")
|
logger.trace("linking modules...")
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ case class ResolveImportsErr[I, E, S[_]](fromFile: I, token: Token[S], err: E)
|
|||||||
extends AquaError[I, E, S]
|
extends AquaError[I, E, S]
|
||||||
case class ImportErr[I, E, S[_]](token: Token[S]) extends AquaError[I, E, S]
|
case class ImportErr[I, E, S[_]](token: Token[S]) extends AquaError[I, E, S]
|
||||||
|
|
||||||
case class CycleError[I, E, S[_]](modules: List[I]) extends AquaError[I, E, S]
|
case class CycleError[I, E, S[_]](modules: NonEmptyChain[I]) extends AquaError[I, E, S]
|
||||||
|
|
||||||
case class CompileError[I, E, S[_]](err: SemanticError[S]) extends AquaError[I, E, S]
|
case class CompileError[I, E, S[_]](err: SemanticError[S]) extends AquaError[I, E, S]
|
||||||
case class OutputError[I, E, S[_]](compiled: AquaCompiled[I], err: E) extends AquaError[I, E, S]
|
case class OutputError[I, E, S[_]](compiled: AquaCompiled[I], err: E) extends AquaError[I, E, S]
|
||||||
|
@ -75,7 +75,16 @@ object ErrorRendering {
|
|||||||
val span = token.unit._1
|
val span = token.unit._1
|
||||||
showForConsole("Cannot resolve import", span, "Cannot resolve import" :: Nil)
|
showForConsole("Cannot resolve import", span, "Cannot resolve import" :: Nil)
|
||||||
case CycleError(modules) =>
|
case CycleError(modules) =>
|
||||||
s"Cycle loops detected in imports: ${modules.map(_.file.fileName)}"
|
val cycleFileNames = (
|
||||||
|
modules.toChain.toList :+ modules.head
|
||||||
|
).map(_.file.fileName)
|
||||||
|
val message = cycleFileNames
|
||||||
|
.sliding(2)
|
||||||
|
.collect { case prev :: next :: Nil =>
|
||||||
|
s"$prev imports $next"
|
||||||
|
}
|
||||||
|
.mkString(", ")
|
||||||
|
s"Cycle loops detected in imports: $message"
|
||||||
case CompileError(err) =>
|
case CompileError(err) =>
|
||||||
err match {
|
err match {
|
||||||
case RulesViolated(token, messages) =>
|
case RulesViolated(token, messages) =>
|
||||||
|
@ -2,22 +2,91 @@ package aqua.linker
|
|||||||
|
|
||||||
import cats.data.{NonEmptyChain, Validated, ValidatedNec}
|
import cats.data.{NonEmptyChain, Validated, ValidatedNec}
|
||||||
import cats.kernel.{Monoid, Semigroup}
|
import cats.kernel.{Monoid, Semigroup}
|
||||||
import cats.syntax.semigroup._
|
import cats.syntax.semigroup.*
|
||||||
|
import cats.syntax.validated.*
|
||||||
|
import cats.syntax.functor.*
|
||||||
|
import cats.instances.list.*
|
||||||
import scribe.Logging
|
import scribe.Logging
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
|
|
||||||
object Linker extends Logging {
|
object Linker extends Logging {
|
||||||
|
|
||||||
|
// Dependency Cycle, prev element import next
|
||||||
|
// and last imports head
|
||||||
|
type DepCycle[I] = NonEmptyChain[I]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find dependecy cycles in modules
|
||||||
|
*
|
||||||
|
* @param mods Modules
|
||||||
|
* @return [[List]] of dependecy cycles found
|
||||||
|
*/
|
||||||
|
private def findDepCycles[I, E, T](
|
||||||
|
mods: List[AquaModule[I, E, T => T]]
|
||||||
|
): List[DepCycle[AquaModule[I, E, T => T]]] = {
|
||||||
|
val modsIds = mods.map(_.id).toSet
|
||||||
|
// Limit search to only passed modules (there maybe dependencies not from `mods`)
|
||||||
|
val deps = mods.map(m => m.id -> m.dependsOn.keySet.intersect(modsIds)).toMap
|
||||||
|
|
||||||
|
// DFS traversal of dependency graph
|
||||||
|
@tailrec
|
||||||
|
def findCycles(
|
||||||
|
paths: List[NonEmptyChain[I]],
|
||||||
|
visited: Set[I],
|
||||||
|
result: List[DepCycle[I]]
|
||||||
|
): List[DepCycle[I]] = paths match {
|
||||||
|
case Nil => result
|
||||||
|
case path :: otherPaths =>
|
||||||
|
val pathDeps = deps.get(path.last).toList.flatten
|
||||||
|
val cycles = pathDeps.flatMap(dep =>
|
||||||
|
NonEmptyChain
|
||||||
|
.fromChain(
|
||||||
|
// This is slow
|
||||||
|
path.toChain.dropWhile(_ != dep)
|
||||||
|
)
|
||||||
|
.toList
|
||||||
|
)
|
||||||
|
val newPaths = pathDeps
|
||||||
|
.filterNot(visited.contains)
|
||||||
|
.map(path :+ _) ++ otherPaths
|
||||||
|
|
||||||
|
findCycles(
|
||||||
|
paths = newPaths,
|
||||||
|
visited = visited ++ pathDeps,
|
||||||
|
result = cycles ++ result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cycles = mods
|
||||||
|
.flatMap(m =>
|
||||||
|
findCycles(
|
||||||
|
paths = NonEmptyChain.one(m.id) :: Nil,
|
||||||
|
visited = Set.empty,
|
||||||
|
result = List.empty
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.distinctBy(
|
||||||
|
// This is really slow, but there
|
||||||
|
// should not be a lot of cycles
|
||||||
|
_.toChain.toList.toSet
|
||||||
|
)
|
||||||
|
|
||||||
|
val modsById = mods.fproductLeft(_.id).toMap
|
||||||
|
|
||||||
|
// This should be safe
|
||||||
|
cycles.map(_.map(modsById))
|
||||||
|
}
|
||||||
|
|
||||||
@tailrec
|
@tailrec
|
||||||
def iter[I, E, T: Semigroup](
|
def iter[I, E, T: Semigroup](
|
||||||
mods: List[AquaModule[I, E, T => T]],
|
mods: List[AquaModule[I, E, T => T]],
|
||||||
proc: Map[I, T => T],
|
proc: Map[I, T => T],
|
||||||
cycleError: List[AquaModule[I, E, T => T]] => E
|
cycleError: DepCycle[AquaModule[I, E, T => T]] => E
|
||||||
): Either[E, Map[I, T => T]] =
|
): ValidatedNec[E, Map[I, T => T]] =
|
||||||
mods match {
|
mods match {
|
||||||
case Nil =>
|
case Nil =>
|
||||||
Right(proc)
|
proc.valid
|
||||||
case _ =>
|
case _ =>
|
||||||
val (canHandle, postpone) = mods.partition(_.dependsOn.keySet.forall(proc.contains))
|
val (canHandle, postpone) = mods.partition(_.dependsOn.keySet.forall(proc.contains))
|
||||||
logger.debug("ITERATE, can handle: " + canHandle.map(_.id))
|
logger.debug("ITERATE, can handle: " + canHandle.map(_.id))
|
||||||
@ -25,9 +94,15 @@ object Linker extends Logging {
|
|||||||
logger.debug(s"postpone = ${postpone.map(_.id)}")
|
logger.debug(s"postpone = ${postpone.map(_.id)}")
|
||||||
logger.debug(s"proc = ${proc.keySet}")
|
logger.debug(s"proc = ${proc.keySet}")
|
||||||
|
|
||||||
if (canHandle.isEmpty && postpone.nonEmpty)
|
if (canHandle.isEmpty && postpone.nonEmpty) {
|
||||||
Left(cycleError(postpone))
|
findDepCycles(postpone)
|
||||||
else {
|
.map(cycleError)
|
||||||
|
.invalid
|
||||||
|
.leftMap(
|
||||||
|
// This should be safe as cycles should exist at this moment
|
||||||
|
errs => NonEmptyChain.fromSeq(errs).get
|
||||||
|
)
|
||||||
|
} else {
|
||||||
val folded = canHandle.foldLeft(proc) { case (acc, m) =>
|
val folded = canHandle.foldLeft(proc) { case (acc, m) =>
|
||||||
val importKeys = m.dependsOn.keySet
|
val importKeys = m.dependsOn.keySet
|
||||||
logger.debug(s"${m.id} dependsOn $importKeys")
|
logger.debug(s"${m.id} dependsOn $importKeys")
|
||||||
@ -52,19 +127,14 @@ object Linker extends Logging {
|
|||||||
|
|
||||||
def link[I, E, T: Semigroup](
|
def link[I, E, T: Semigroup](
|
||||||
modules: Modules[I, E, T => T],
|
modules: Modules[I, E, T => T],
|
||||||
cycleError: List[AquaModule[I, E, T => T]] => E,
|
cycleError: DepCycle[AquaModule[I, E, T => T]] => E,
|
||||||
empty: I => T
|
empty: I => T
|
||||||
): ValidatedNec[E, Map[I, T]] =
|
): ValidatedNec[E, Map[I, T]] =
|
||||||
if (modules.dependsOn.nonEmpty) Validated.invalid(modules.dependsOn.values.reduce(_ ++ _))
|
if (modules.dependsOn.nonEmpty) Validated.invalid(modules.dependsOn.values.reduce(_ ++ _))
|
||||||
else {
|
else {
|
||||||
val result = iter(modules.loaded.values.toList, Map.empty[I, T => T], cycleError)
|
val result = iter(modules.loaded.values.toList, Map.empty, cycleError)
|
||||||
|
|
||||||
Validated.fromEither(
|
result.map(_.collect { case (i, f) if modules.exports(i) => i -> f(empty(i)) })
|
||||||
result
|
|
||||||
.map(_.collect { case (i, f) if modules.exports(i) => i -> f(empty(i)) })
|
|
||||||
.left
|
|
||||||
.map(NonEmptyChain.one)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,10 @@ class LinkerSpec extends AnyFlatSpec with Matchers {
|
|||||||
empty
|
empty
|
||||||
.add(
|
.add(
|
||||||
AquaModule[String, String, String => String](
|
AquaModule[String, String, String => String](
|
||||||
"mod1",
|
id = "mod1",
|
||||||
Map.empty,
|
imports = Map.empty,
|
||||||
Map("mod2" -> "unresolved mod2 in mod1"),
|
dependsOn = Map("mod2" -> "unresolved mod2 in mod1"),
|
||||||
_ ++ " | mod1"
|
body = _ ++ " | mod1"
|
||||||
),
|
),
|
||||||
toExport = true
|
toExport = true
|
||||||
)
|
)
|
||||||
@ -25,7 +25,7 @@ class LinkerSpec extends AnyFlatSpec with Matchers {
|
|||||||
|
|
||||||
Linker.link[String, String, String](
|
Linker.link[String, String, String](
|
||||||
withMod1,
|
withMod1,
|
||||||
cycle => cycle.map(_.id).mkString(" -> "),
|
cycle => cycle.map(_.id).toChain.toList.mkString(" -> "),
|
||||||
_ => ""
|
_ => ""
|
||||||
) should be(Validated.invalidNec("unresolved mod2 in mod1"))
|
) should be(Validated.invalidNec("unresolved mod2 in mod1"))
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ class LinkerSpec extends AnyFlatSpec with Matchers {
|
|||||||
|
|
||||||
Linker.link[String, String, String](
|
Linker.link[String, String, String](
|
||||||
withMod2,
|
withMod2,
|
||||||
cycle => cycle.map(_.id + "?").mkString(" -> "),
|
cycle => cycle.map(_.id + "?").toChain.toList.mkString(" -> "),
|
||||||
_ => ""
|
_ => ""
|
||||||
) should be(Validated.validNec(Map("mod1" -> " | mod2 | mod1")))
|
) should be(Validated.validNec(Map("mod1" -> " | mod2 | mod1")))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user