mirror of
https://github.com/fluencelabs/aqua.git
synced 2024-12-04 22:50:18 +00:00
refactor(LNG-147): Rewrite list to tree conversion tail recusive (#711)
* Implemented ListToTreeConverter
This commit is contained in:
parent
440899f6c6
commit
3af28d5e53
@ -15,6 +15,8 @@ import cats.parse.{Parser as P, Parser0 as P0}
|
||||
import cats.syntax.comonad.*
|
||||
import cats.{~>, Comonad, Eval}
|
||||
import scribe.Logging
|
||||
import aqua.parser.Ast.Tree
|
||||
import aqua.parser.ListToTreeConverter
|
||||
|
||||
abstract class Expr[F[_]](val companion: Expr.Companion, val token: Token[F]) {
|
||||
|
||||
@ -92,143 +94,19 @@ object Expr {
|
||||
abstract class AndIndented extends Block with Logging {
|
||||
def validChildren: List[Lexem]
|
||||
|
||||
private def leaf[F[_]](expr: Expr[F]): Ast.Tree[F] =
|
||||
Cofree[Chain, Expr[F]](
|
||||
expr,
|
||||
Eval.now(Chain.empty)
|
||||
)
|
||||
|
||||
private def last[F[_]](tree: Ast.Tree[F]): Expr[F] =
|
||||
tree.tailForced.lastOption.fold(tree.head)(last)
|
||||
|
||||
private def setLeafs[F[_]](tree: Ast.Tree[F], children: Chain[Tree[F]]): Tree[F] =
|
||||
tree.copy(tail = tree.tail.map {
|
||||
case pref :== last =>
|
||||
pref :+ setLeafs(last, children)
|
||||
case _ =>
|
||||
children
|
||||
})
|
||||
|
||||
// Check if expression can be added in current block
|
||||
private def canAddToBlock[F[_]](block: Tree[F], expr: Expr[F]): Boolean = {
|
||||
block.head.companion match {
|
||||
case b: AndIndented =>
|
||||
b.validChildren.map {
|
||||
case ll: LazyLexem => ll.c
|
||||
case vc => vc
|
||||
}.contains(expr.companion)
|
||||
|
||||
case _: Prefix =>
|
||||
block.tail.value.headOption.exists(t => canAddToBlock(t, expr))
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
// Generate error if expression (child) cannot be added to a block
|
||||
private def wrongChildError[F[_]](indent: F[String], expr: Expr[F]): ParserError[F] = {
|
||||
val msg = expr match {
|
||||
case ReturnExpr(_) =>
|
||||
"Return expression must be on the top indentation level and at the end of function body"
|
||||
// could there be other expressions?
|
||||
case _ => "This expression is on the wrong indentation level"
|
||||
}
|
||||
BlockIndentError(indent, msg)
|
||||
}
|
||||
|
||||
private def headIsBlock[F[_]](tree: Tree[F]): Boolean = {
|
||||
tree.tail.value.headOption match {
|
||||
case Some(t) => t.head.isBlock
|
||||
case _ => tree.head.isBlock
|
||||
}
|
||||
}
|
||||
|
||||
private case class Acc[F[_]](
|
||||
block: Tree[F],
|
||||
initialIndentF: F[String],
|
||||
tail: Chain[(F[String], Ast.Tree[F])] = Chain.empty[(F[String], Ast.Tree[F])],
|
||||
window: Chain[Tree[F]] = Chain.empty[Tree[F]],
|
||||
errors: Chain[ParserError[F]] = Chain.empty[ParserError[F]]
|
||||
)
|
||||
|
||||
// converts list of expressions to a tree of tokens
|
||||
private def listToTree[F[_]: Comonad: LiftParser](
|
||||
acc: Acc[F]
|
||||
): ValidatedNec[ParserError[F], Acc[F]] = {
|
||||
val initialIndent = acc.initialIndentF.extract.length
|
||||
|
||||
acc.tail.uncons match {
|
||||
case Some(((currentIndent, currentExpr), tail)) =>
|
||||
val current = last(currentExpr)
|
||||
|
||||
// if current indent is bigger then block indentation
|
||||
// then add current expression to this block
|
||||
if (currentIndent.extract.length > initialIndent) {
|
||||
// if current expression is a block, create tree of this block and return remaining tail
|
||||
if (headIsBlock(currentExpr)) {
|
||||
listToTree(Acc(currentExpr, currentIndent, tail, errors = acc.errors)).andThen {
|
||||
case a@Acc(innerTree, _, newTail, window, errors) =>
|
||||
if (window.nonEmpty) {
|
||||
logger.warn("Internal: Window cannot be empty after converting list of expressions to a tree.")
|
||||
logger.warn("Current state: " + a)
|
||||
}
|
||||
|
||||
listToTree(
|
||||
acc.copy(
|
||||
window = acc.window :+ innerTree,
|
||||
tail = newTail,
|
||||
errors = acc.errors ++ errors
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// if expression not a block, add it to a window until we meet the end of the block
|
||||
if (canAddToBlock(acc.block, current)) {
|
||||
listToTree(acc.copy(window = acc.window :+ currentExpr, tail = tail))
|
||||
} else {
|
||||
val error = wrongChildError(currentIndent, current)
|
||||
validNec(acc.copy(tail = tail, errors = acc.errors :+ error))
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
val errors = if (acc.window.isEmpty) {
|
||||
// error if a block is empty
|
||||
val error = BlockIndentError(acc.initialIndentF, "Block expression has no body")
|
||||
acc.errors :+ error
|
||||
} else acc.errors
|
||||
|
||||
// if current indentation less or equal to block indentation,
|
||||
// add all expressions in window to a head
|
||||
validNec(
|
||||
Acc(
|
||||
setLeafs(acc.block, acc.window),
|
||||
acc.initialIndentF,
|
||||
(currentIndent, currentExpr) +: tail,
|
||||
errors = errors
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
case None =>
|
||||
// end of top-level block
|
||||
NonEmptyChain
|
||||
.fromChain(acc.errors)
|
||||
.map(invalid)
|
||||
.getOrElse(validNec(Acc(setLeafs(acc.block, acc.window), acc.initialIndentF)))
|
||||
}
|
||||
}
|
||||
|
||||
override lazy val ast: P[ValidatedNec[ParserError[Span.S], Ast.Tree[Span.S]]] =
|
||||
(readLine ~ (` \n+` *>
|
||||
(P.repSep(
|
||||
` `.lift ~ P.oneOf(validChildren.map(_.readLine.backtrack)),
|
||||
` \n+`
|
||||
) <* ` \n`.?))).map { t =>
|
||||
val startIndent = t._1.head.token.as("")
|
||||
listToTree(Acc(t._1, startIndent, Chain.fromSeq(t._2.toList))).map { res =>
|
||||
res._1
|
||||
) <* ` \n`.?))).map { case (open, lines) =>
|
||||
lines
|
||||
.foldLeft(
|
||||
ListToTreeConverter(open)
|
||||
) { case (acc, (indent, line)) =>
|
||||
acc.next(indent, line)
|
||||
}
|
||||
.result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
188
parser/src/main/scala/aqua/parser/ListToTreeConverter.scala
Normal file
188
parser/src/main/scala/aqua/parser/ListToTreeConverter.scala
Normal file
@ -0,0 +1,188 @@
|
||||
package aqua.parser
|
||||
|
||||
import aqua.parser.Ast.Tree
|
||||
import aqua.parser.Expr.{AndIndented, LazyLexem, Prefix}
|
||||
import aqua.parser.expr.func.ReturnExpr
|
||||
import aqua.parser.lift.LiftParser
|
||||
|
||||
import cats.Comonad
|
||||
import cats.data.Chain
|
||||
import cats.data.Chain.:==
|
||||
import cats.syntax.comonad.*
|
||||
import cats.data.{NonEmptyChain, ValidatedNec}
|
||||
import cats.data.Validated.*
|
||||
import aqua.parser.Expr.LazyLexem
|
||||
|
||||
/**
|
||||
* Converts a list of lines to a tree
|
||||
*/
|
||||
final case class ListToTreeConverter[F[_]](
|
||||
currentBlock: ListToTreeConverter.Block[F], // Current block data
|
||||
stack: List[ListToTreeConverter.Block[F]] = Nil, // Stack of opened blocks
|
||||
errors: Chain[ParserError[F]] = Chain.empty[ParserError[F]] // Errors
|
||||
)(using Comonad[F]) {
|
||||
// Import helper functions
|
||||
import ListToTreeConverter.*
|
||||
|
||||
private def addError(error: ParserError[F]): ListToTreeConverter[F] =
|
||||
copy(errors = errors :+ error)
|
||||
|
||||
private def pushBlock(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
||||
copy(currentBlock = Block(indent, line), stack = currentBlock :: stack)
|
||||
|
||||
private def addToCurrentBlock(line: Tree[F]): ListToTreeConverter[F] =
|
||||
copy(currentBlock = currentBlock.add(line))
|
||||
|
||||
private def popBlock: Option[ListToTreeConverter[F]] =
|
||||
stack match {
|
||||
case Nil => None
|
||||
case prevBlock :: tail =>
|
||||
Some(copy(currentBlock = prevBlock.add(currentBlock.close), stack = tail))
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to call on each new line
|
||||
*/
|
||||
@scala.annotation.tailrec
|
||||
def next(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
||||
if (indentValue(indent) > indentValue(currentBlock.indent)) {
|
||||
if (isBlock(line)) {
|
||||
pushBlock(indent, line)
|
||||
} else {
|
||||
val expr = lastExpr(line)
|
||||
|
||||
if (currentBlock.canAdd(expr)) {
|
||||
addToCurrentBlock(line)
|
||||
} else {
|
||||
addError(wrongChildError(indent, expr))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val emptyChecked = if (currentBlock.isEmpty) {
|
||||
addError(emptyBlockError(currentBlock.indent))
|
||||
} else this
|
||||
|
||||
emptyChecked.popBlock match {
|
||||
case Some(blockPopped) => blockPopped.next(indent, line)
|
||||
// This should not happen because of the way of parsing
|
||||
case _ => emptyChecked.addError(unexpectedIndentError(indent))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce the result of the conversion
|
||||
*/
|
||||
@scala.annotation.tailrec
|
||||
def result: ValidatedNec[ParserError[F], Tree[F]] =
|
||||
popBlock match {
|
||||
case Some(blockPopped) => blockPopped.result
|
||||
case _ =>
|
||||
NonEmptyChain
|
||||
.fromChain(errors)
|
||||
.map(invalid)
|
||||
.getOrElse(
|
||||
valid(currentBlock.close)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object ListToTreeConverter {
|
||||
|
||||
/**
|
||||
* Constructs a converter from block opening line
|
||||
*/
|
||||
def apply[F[_]](open: Tree[F])(using Comonad[F]): ListToTreeConverter[F] =
|
||||
ListToTreeConverter(Block(open.head.token.as(""), open))
|
||||
|
||||
/**
|
||||
* Data associated with a block
|
||||
*/
|
||||
final case class Block[F[_]](
|
||||
indent: F[String], // Indentation of the block opening line
|
||||
block: Tree[F], // Block opening line
|
||||
content: Chain[Tree[F]] = Chain.empty[Tree[F]] // Children of the block
|
||||
) {
|
||||
|
||||
/**
|
||||
* Check if expr can be added to this block
|
||||
*/
|
||||
def canAdd(expr: Expr[F]): Boolean = {
|
||||
def checkFor(tree: Tree[F]): Boolean =
|
||||
tree.head.companion match {
|
||||
case indented: AndIndented =>
|
||||
indented.validChildren.map {
|
||||
case ll: LazyLexem => ll.c
|
||||
case vc => vc
|
||||
}.contains(expr.companion)
|
||||
case _: Prefix =>
|
||||
tree.tail.value.headOption.exists(checkFor)
|
||||
case _ => false
|
||||
}
|
||||
|
||||
checkFor(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add child to the block
|
||||
*/
|
||||
def add(child: Tree[F]): Block[F] =
|
||||
copy(content = content :+ child)
|
||||
|
||||
/**
|
||||
* Check if the block has no children
|
||||
*/
|
||||
def isEmpty: Boolean = content.isEmpty
|
||||
|
||||
/**
|
||||
* Create a tree corresponding to the block
|
||||
*/
|
||||
def close: Tree[F] = {
|
||||
|
||||
/**
|
||||
* Set children of the rightmost expression in tree
|
||||
*/
|
||||
def setLast(tree: Tree[F], children: Chain[Tree[F]]): Tree[F] =
|
||||
tree.copy(tail = tree.tail.map {
|
||||
case init :== last =>
|
||||
init :+ setLast(last, children)
|
||||
case _ =>
|
||||
children
|
||||
})
|
||||
|
||||
setLast(block, content)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(LNG-150): This way of counting indent does not play way with mixed tabs and spaces
|
||||
private def indentValue[F[_]](indent: F[String])(using Comonad[F]): Int =
|
||||
indent.extract.length
|
||||
|
||||
private def isBlock[F[_]](line: Tree[F]): Boolean =
|
||||
line.tail.value.headOption
|
||||
.map(_.head.isBlock)
|
||||
.getOrElse(line.head.isBlock)
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def lastExpr[F[_]](tree: Tree[F]): Expr[F] =
|
||||
tree.tailForced.lastOption match {
|
||||
case Some(t) => lastExpr(t)
|
||||
case _ => tree.head
|
||||
}
|
||||
|
||||
def wrongChildError[F[_]](indent: F[String], expr: Expr[F]): ParserError[F] = {
|
||||
val msg = expr match {
|
||||
case ReturnExpr(_) =>
|
||||
"Return expression must be on the top indentation level and at the end of function body"
|
||||
// could there be other expressions?
|
||||
case _ => "This expression is on the wrong indentation level"
|
||||
}
|
||||
BlockIndentError(indent, msg)
|
||||
}
|
||||
|
||||
def emptyBlockError[F[_]](indent: F[String]): ParserError[F] =
|
||||
BlockIndentError(indent, "Block expression has no body")
|
||||
|
||||
def unexpectedIndentError[F[_]](indent: F[String]): ParserError[F] =
|
||||
BlockIndentError(indent, "Unexpected indentation")
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user