mirror of
https://github.com/fluencelabs/aqua.git
synced 2025-03-15 19:50:51 +00:00
feat(parser): Strict indentation [fixes LNG-135] (#714)
* Implemented indentation consistency checking * Added comments --------- Co-authored-by: Dima <dmitry.shakhtarin@fluence.ai>
This commit is contained in:
parent
32430f445d
commit
ae2a433185
@ -21,6 +21,7 @@ final case class ListToTreeConverter[F[_]](
|
||||
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.*
|
||||
|
||||
@ -30,14 +31,22 @@ final case class ListToTreeConverter[F[_]](
|
||||
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 addToCurrentBlock(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
||||
copy(currentBlock = currentBlock.add(indent, line))
|
||||
|
||||
private def popBlock: Option[ListToTreeConverter[F]] =
|
||||
stack match {
|
||||
case Nil => None
|
||||
case prevBlock :: tail =>
|
||||
Some(copy(currentBlock = prevBlock.add(currentBlock.close), stack = tail))
|
||||
Some(
|
||||
copy(
|
||||
currentBlock = prevBlock.add(
|
||||
currentBlock.indent,
|
||||
currentBlock.close
|
||||
),
|
||||
stack = tail
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,28 +54,37 @@ final case class ListToTreeConverter[F[_]](
|
||||
*/
|
||||
@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)
|
||||
currentBlock.classifyIndent(indent) match {
|
||||
case IndentRelation.Child(consistent) =>
|
||||
val consistentChecked = if (!consistent) {
|
||||
addError(inconsistentIndentError(indent))
|
||||
} else this
|
||||
|
||||
if (currentBlock.canAdd(expr)) {
|
||||
addToCurrentBlock(line)
|
||||
if (isBlock(line)) {
|
||||
consistentChecked.pushBlock(indent, line)
|
||||
} else {
|
||||
addError(wrongChildError(indent, expr))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val emptyChecked = if (currentBlock.isEmpty) {
|
||||
addError(emptyBlockError(currentBlock.indent))
|
||||
} else this
|
||||
val expr = lastExpr(line)
|
||||
|
||||
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))
|
||||
}
|
||||
if (currentBlock.isValidChild(expr)) {
|
||||
consistentChecked.addToCurrentBlock(indent, line)
|
||||
} else {
|
||||
consistentChecked.addError(wrongChildError(indent, expr))
|
||||
}
|
||||
}
|
||||
// Note: this doesn't necessarly mean that indentation is correct
|
||||
// next block will check it
|
||||
case IndentRelation.Sibling =>
|
||||
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))
|
||||
}
|
||||
case IndentRelation.Unexpected =>
|
||||
addError(unexpectedIndentError(indent))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,19 +112,55 @@ object ListToTreeConverter {
|
||||
def apply[F[_]](open: Tree[F])(using Comonad[F]): ListToTreeConverter[F] =
|
||||
ListToTreeConverter(Block(open.head.token.as(""), open))
|
||||
|
||||
/**
|
||||
* Describes the realtion of next line to the current block
|
||||
*/
|
||||
enum IndentRelation {
|
||||
case Child(consistent: Boolean)
|
||||
case Sibling
|
||||
case Unexpected
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
childIndent: Option[F[String]] = None, // Indentation of the first child
|
||||
content: Chain[Tree[F]] = Chain.empty[Tree[F]] // Children of the block
|
||||
) {
|
||||
|
||||
/**
|
||||
* Classify the next line relative to the block
|
||||
*/
|
||||
def classifyIndent(lineIndent: F[String])(using Comonad[F]): IndentRelation = {
|
||||
val blockIndentStr = indent.extract
|
||||
val lineIndentStr = lineIndent.extract
|
||||
|
||||
if (lineIndentStr.startsWith(blockIndentStr)) {
|
||||
lazy val consistentChild = childIndent
|
||||
.map(_.extract)
|
||||
.fold(
|
||||
lineIndentStr.length > blockIndentStr.length
|
||||
)(_ == lineIndentStr)
|
||||
|
||||
if (lineIndentStr.length == blockIndentStr.length) {
|
||||
IndentRelation.Sibling
|
||||
} else {
|
||||
IndentRelation.Child(consistentChild)
|
||||
}
|
||||
} else if (blockIndentStr.startsWith(lineIndentStr)) {
|
||||
IndentRelation.Sibling
|
||||
} else {
|
||||
IndentRelation.Unexpected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if expr can be added to this block
|
||||
*/
|
||||
def canAdd(expr: Expr[F]): Boolean = {
|
||||
def isValidChild(expr: Expr[F]): Boolean = {
|
||||
def checkFor(tree: Tree[F]): Boolean =
|
||||
tree.head.companion match {
|
||||
case indented: AndIndented =>
|
||||
@ -123,10 +177,13 @@ object ListToTreeConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add child to the block
|
||||
* Add line to the block
|
||||
*/
|
||||
def add(child: Tree[F]): Block[F] =
|
||||
copy(content = content :+ child)
|
||||
def add(indent: F[String], line: Tree[F]): Block[F] =
|
||||
copy(
|
||||
content = content :+ line,
|
||||
childIndent = childIndent.orElse(Some(indent))
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if the block has no children
|
||||
@ -182,6 +239,9 @@ object ListToTreeConverter {
|
||||
def emptyBlockError[F[_]](indent: F[String]): ParserError[F] =
|
||||
BlockIndentError(indent, "Block expression has no body")
|
||||
|
||||
def inconsistentIndentError[F[_]](indent: F[String]): ParserError[F] =
|
||||
BlockIndentError(indent, "Inconsistent indentation in the block")
|
||||
|
||||
def unexpectedIndentError[F[_]](indent: F[String]): ParserError[F] =
|
||||
BlockIndentError(indent, "Unexpected indentation")
|
||||
|
||||
|
@ -18,7 +18,9 @@ import cats.Id
|
||||
import cats.data.{Chain, NonEmptyList}
|
||||
import cats.free.Cofree
|
||||
import cats.syntax.foldable.*
|
||||
import cats.data.Validated.{Invalid, Valid}
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.{Inside, Inspectors}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import cats.~>
|
||||
|
||||
@ -27,7 +29,7 @@ import scala.language.implicitConversions
|
||||
import aqua.parser.lift.Span
|
||||
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
|
||||
|
||||
class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
|
||||
class FuncExprSpec extends AnyFlatSpec with Matchers with Inside with Inspectors with AquaSpec {
|
||||
import AquaSpec._
|
||||
|
||||
private val parser = Parser.spanParser
|
||||
@ -116,16 +118,87 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
|
||||
|
||||
}
|
||||
|
||||
// TODO: unignore in LNG-135
|
||||
"function with wrong indent" should "parse with error" ignore {
|
||||
val script =
|
||||
"""func tryGen() -> bool:
|
||||
| on "deeper" via "deep":
|
||||
| v <- Local.gt()
|
||||
"function with mixed blocks indent" should "parse without error" in {
|
||||
val scriptFor =
|
||||
"""func try():
|
||||
| v <- Local.call()
|
||||
| for x <- v:
|
||||
| foo(x)
|
||||
| for y <- x:
|
||||
| bar(y)
|
||||
| for z <- v:
|
||||
| baz(z)
|
||||
|""".stripMargin
|
||||
|
||||
val scriptIf =
|
||||
"""func try(v: bool):
|
||||
| if v:
|
||||
| foo()
|
||||
| else:
|
||||
| if v:
|
||||
| bar()
|
||||
| else:
|
||||
| baz()
|
||||
|""".stripMargin
|
||||
|
||||
val scriptOn =
|
||||
"""func try():
|
||||
| on "some" via "some":
|
||||
| foo()
|
||||
| on "some" via "some":
|
||||
| bar()
|
||||
| on "some" via "some":
|
||||
| bar()
|
||||
|""".stripMargin
|
||||
|
||||
forAll(List(scriptFor, scriptIf, scriptOn)) { script =>
|
||||
parser.parseAll(script).value.toEither.isRight shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
"function with wrong indent" should "parse with error" in {
|
||||
val scriptSimple =
|
||||
"""func try():
|
||||
| v <- Local.call()
|
||||
| x = v
|
||||
|""".stripMargin
|
||||
|
||||
val scriptReturn =
|
||||
"""func try() -> bool:
|
||||
| v <- Local.call()
|
||||
| <- v
|
||||
|""".stripMargin
|
||||
|
||||
parser.parseAll(script).value.toEither.isLeft shouldBe true
|
||||
val scriptFor =
|
||||
"""func try():
|
||||
| v <- call()
|
||||
| for x <- v:
|
||||
| foo(x)
|
||||
|""".stripMargin
|
||||
|
||||
val scriptIf =
|
||||
"""func try(v: bool):
|
||||
| if v:
|
||||
| foo()
|
||||
| call()
|
||||
|""".stripMargin
|
||||
|
||||
val scriptOn =
|
||||
"""func try():
|
||||
| call()
|
||||
| on "some" via "some":
|
||||
| foo()
|
||||
|""".stripMargin
|
||||
|
||||
forAll(List(scriptSimple, scriptReturn, scriptFor, scriptIf, scriptOn)) { script =>
|
||||
inside(parser.parseAll(script).value) { case Invalid(errors) =>
|
||||
forAll(errors.toList) { error =>
|
||||
inside(error) { case BlockIndentError(_, message) =>
|
||||
message shouldEqual "Inconsistent indentation in the block"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"function with multiline definitions" should "parse without error" in {
|
||||
@ -179,7 +252,10 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
|
||||
)
|
||||
qTree.d() shouldBe ArrowExpr(toArrowType(Nil, Some(scToBt(bool))))
|
||||
qTree.d() shouldBe OnExpr(toStr("deeper"), List(toStr("deep")))
|
||||
qTree.d() shouldBe CallArrowExpr(List("v"), CallArrowToken(Some(toNamedType("Local")), "gt", Nil))
|
||||
qTree.d() shouldBe CallArrowExpr(
|
||||
List("v"),
|
||||
CallArrowToken(Some(toNamedType("Local")), "gt", Nil)
|
||||
)
|
||||
qTree.d() shouldBe ReturnExpr(NonEmptyList.one(toVar("v")))
|
||||
// genC function
|
||||
qTree.d() shouldBe FuncExpr(
|
||||
@ -188,7 +264,10 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
|
||||
// List("two": VarLambda[Id])
|
||||
)
|
||||
qTree.d() shouldBe ArrowExpr(toNamedArrow(("val" -> string) :: Nil, boolSc :: Nil))
|
||||
qTree.d() shouldBe CallArrowExpr(List("one"), CallArrowToken(Some(toNamedType("Local")), "gt", List()))
|
||||
qTree.d() shouldBe CallArrowExpr(
|
||||
List("one"),
|
||||
CallArrowToken(Some(toNamedType("Local")), "gt", List())
|
||||
)
|
||||
qTree.d() shouldBe OnExpr(toStr("smth"), List(toStr("else")))
|
||||
qTree.d() shouldBe CallArrowExpr(List("two"), CallArrowToken(None, "tryGen", List()))
|
||||
qTree.d() shouldBe CallArrowExpr(
|
||||
|
Loading…
x
Reference in New Issue
Block a user