mirror of
https://github.com/fluencelabs/aqua.git
synced 2025-03-15 11:40:50 +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
|
stack: List[ListToTreeConverter.Block[F]] = Nil, // Stack of opened blocks
|
||||||
errors: Chain[ParserError[F]] = Chain.empty[ParserError[F]] // Errors
|
errors: Chain[ParserError[F]] = Chain.empty[ParserError[F]] // Errors
|
||||||
)(using Comonad[F]) {
|
)(using Comonad[F]) {
|
||||||
|
|
||||||
// Import helper functions
|
// Import helper functions
|
||||||
import ListToTreeConverter.*
|
import ListToTreeConverter.*
|
||||||
|
|
||||||
@ -30,14 +31,22 @@ final case class ListToTreeConverter[F[_]](
|
|||||||
private def pushBlock(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
private def pushBlock(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
||||||
copy(currentBlock = Block(indent, line), stack = currentBlock :: stack)
|
copy(currentBlock = Block(indent, line), stack = currentBlock :: stack)
|
||||||
|
|
||||||
private def addToCurrentBlock(line: Tree[F]): ListToTreeConverter[F] =
|
private def addToCurrentBlock(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
||||||
copy(currentBlock = currentBlock.add(line))
|
copy(currentBlock = currentBlock.add(indent, line))
|
||||||
|
|
||||||
private def popBlock: Option[ListToTreeConverter[F]] =
|
private def popBlock: Option[ListToTreeConverter[F]] =
|
||||||
stack match {
|
stack match {
|
||||||
case Nil => None
|
case Nil => None
|
||||||
case prevBlock :: tail =>
|
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
|
@scala.annotation.tailrec
|
||||||
def next(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
def next(indent: F[String], line: Tree[F]): ListToTreeConverter[F] =
|
||||||
if (indentValue(indent) > indentValue(currentBlock.indent)) {
|
currentBlock.classifyIndent(indent) match {
|
||||||
if (isBlock(line)) {
|
case IndentRelation.Child(consistent) =>
|
||||||
pushBlock(indent, line)
|
val consistentChecked = if (!consistent) {
|
||||||
} else {
|
addError(inconsistentIndentError(indent))
|
||||||
val expr = lastExpr(line)
|
} else this
|
||||||
|
|
||||||
if (currentBlock.canAdd(expr)) {
|
if (isBlock(line)) {
|
||||||
addToCurrentBlock(line)
|
consistentChecked.pushBlock(indent, line)
|
||||||
} else {
|
} else {
|
||||||
addError(wrongChildError(indent, expr))
|
val expr = lastExpr(line)
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val emptyChecked = if (currentBlock.isEmpty) {
|
|
||||||
addError(emptyBlockError(currentBlock.indent))
|
|
||||||
} else this
|
|
||||||
|
|
||||||
emptyChecked.popBlock match {
|
if (currentBlock.isValidChild(expr)) {
|
||||||
case Some(blockPopped) => blockPopped.next(indent, line)
|
consistentChecked.addToCurrentBlock(indent, line)
|
||||||
// This should not happen because of the way of parsing
|
} else {
|
||||||
case _ => emptyChecked.addError(unexpectedIndentError(indent))
|
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] =
|
def apply[F[_]](open: Tree[F])(using Comonad[F]): ListToTreeConverter[F] =
|
||||||
ListToTreeConverter(Block(open.head.token.as(""), open))
|
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
|
* Data associated with a block
|
||||||
*/
|
*/
|
||||||
final case class Block[F[_]](
|
final case class Block[F[_]](
|
||||||
indent: F[String], // Indentation of the block opening line
|
indent: F[String], // Indentation of the block opening line
|
||||||
block: Tree[F], // 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
|
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
|
* 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 =
|
def checkFor(tree: Tree[F]): Boolean =
|
||||||
tree.head.companion match {
|
tree.head.companion match {
|
||||||
case indented: AndIndented =>
|
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] =
|
def add(indent: F[String], line: Tree[F]): Block[F] =
|
||||||
copy(content = content :+ child)
|
copy(
|
||||||
|
content = content :+ line,
|
||||||
|
childIndent = childIndent.orElse(Some(indent))
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the block has no children
|
* Check if the block has no children
|
||||||
@ -182,6 +239,9 @@ object ListToTreeConverter {
|
|||||||
def emptyBlockError[F[_]](indent: F[String]): ParserError[F] =
|
def emptyBlockError[F[_]](indent: F[String]): ParserError[F] =
|
||||||
BlockIndentError(indent, "Block expression has no body")
|
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] =
|
def unexpectedIndentError[F[_]](indent: F[String]): ParserError[F] =
|
||||||
BlockIndentError(indent, "Unexpected indentation")
|
BlockIndentError(indent, "Unexpected indentation")
|
||||||
|
|
||||||
|
@ -18,7 +18,9 @@ import cats.Id
|
|||||||
import cats.data.{Chain, NonEmptyList}
|
import cats.data.{Chain, NonEmptyList}
|
||||||
import cats.free.Cofree
|
import cats.free.Cofree
|
||||||
import cats.syntax.foldable.*
|
import cats.syntax.foldable.*
|
||||||
|
import cats.data.Validated.{Invalid, Valid}
|
||||||
import org.scalatest.flatspec.AnyFlatSpec
|
import org.scalatest.flatspec.AnyFlatSpec
|
||||||
|
import org.scalatest.{Inside, Inspectors}
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
import cats.~>
|
import cats.~>
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ import scala.language.implicitConversions
|
|||||||
import aqua.parser.lift.Span
|
import aqua.parser.lift.Span
|
||||||
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
|
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._
|
import AquaSpec._
|
||||||
|
|
||||||
private val parser = Parser.spanParser
|
private val parser = Parser.spanParser
|
||||||
@ -116,16 +118,87 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: unignore in LNG-135
|
"function with mixed blocks indent" should "parse without error" in {
|
||||||
"function with wrong indent" should "parse with error" ignore {
|
val scriptFor =
|
||||||
val script =
|
"""func try():
|
||||||
"""func tryGen() -> bool:
|
| v <- Local.call()
|
||||||
| on "deeper" via "deep":
|
| for x <- v:
|
||||||
| v <- Local.gt()
|
| 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
|
| <- v
|
||||||
|""".stripMargin
|
|""".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 {
|
"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 ArrowExpr(toArrowType(Nil, Some(scToBt(bool))))
|
||||||
qTree.d() shouldBe OnExpr(toStr("deeper"), List(toStr("deep")))
|
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")))
|
qTree.d() shouldBe ReturnExpr(NonEmptyList.one(toVar("v")))
|
||||||
// genC function
|
// genC function
|
||||||
qTree.d() shouldBe FuncExpr(
|
qTree.d() shouldBe FuncExpr(
|
||||||
@ -188,7 +264,10 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
|
|||||||
// List("two": VarLambda[Id])
|
// List("two": VarLambda[Id])
|
||||||
)
|
)
|
||||||
qTree.d() shouldBe ArrowExpr(toNamedArrow(("val" -> string) :: Nil, boolSc :: Nil))
|
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 OnExpr(toStr("smth"), List(toStr("else")))
|
||||||
qTree.d() shouldBe CallArrowExpr(List("two"), CallArrowToken(None, "tryGen", List()))
|
qTree.d() shouldBe CallArrowExpr(List("two"), CallArrowToken(None, "tryGen", List()))
|
||||||
qTree.d() shouldBe CallArrowExpr(
|
qTree.d() shouldBe CallArrowExpr(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user