Czy istnieje wygodny sposób użycia kombinatorów parsera Scala do parsowania języków, w których wcięcie jest znaczące? (Np Python)Parsowanie języka opartego na wcięciach przy użyciu kombinatorów paralizatora scala
Odpowiedz
Załóżmy, że mamy bardzo prostym językiem, gdzie jest to poprawny program
block
inside
the
block
i chcemy analizować to do List[String]
z każdej linii wewnątrz bloku jako jednego String
.
Najpierw definiujemy metodę, która ma minimalny poziom wcięcia i zwraca analizator składni dla linii o tym poziomie wcięcia.
def line(minIndent:Int):Parser[String] =
repN(minIndent + 1,"\\s".r) ~ ".*".r ^^ {case s ~ r => s.mkString + r}
Następnie określają blok z minimalnym poziomie wcięcia powtarzając parser linii z odpowiednim separatorze między liniami.
def lines(minIndent:Int):Parser[List[String]] =
rep1sep(line(minIndent), "[\n\r]|(\n\r)".r)
Teraz możemy zdefiniować parser dla naszego małego języka tak:
val block:Parser[List[String]] =
(("\\s*".r <~ "block\\n".r) ^^ { _.size }) >> lines
Najpierw określa aktualny poziom wcięcia, a następnie przekazuje, że jako minimum do parsera linii. Przetestujmy go:
val s =
"""block
inside
the
block
outside
the
block"""
println(block(new CharSequenceReader(s)))
i otrzymujemy
[4.10] parsed: List( inside, the, block)
Za to wszystko skompilować, trzeba przywóz
import scala.util.parsing.combinator.RegexParsers
import scala.util.parsing.input.CharSequenceReader
i trzeba postawić wszystko na obiekt, który rozciąga RegexParsers
tak jak
object MyParsers extends RegexParsers {
override def skipWhitespace = false
....
Z tego, co wiem, nie, kombinatory parsera Scala nie mają wsparcia dla tego typu rzeczy po wyjęciu z pudełka. Z pewnością można to zrobić, parsując białą przestrzeń w znaczący sposób, ale napotkasz pewne problemy, ponieważ potrzebujesz jakiejś formy automatu stanów, aby śledzić stos wcięć.
Polecam zrobić krok preprocessing. Tutaj jest trochę preprocesor, który dodaje znaczniki do oddzielenia wcięte bloki:
object Preprocessor {
val BlockStartToken = "{"
val BlockEndToken = "}"
val TabSize = 4 //how many spaces does a tab take
def preProcess(text: String): String = {
val lines = text.split('\n').toList.filterNot(_.forall(isWhiteChar))
val processedLines = BlockStartToken :: insertTokens(lines, List(0))
processedLines.mkString("\n")
}
def insertTokens(lines: List[String], stack: List[Int]): List[String] = lines match {
case List() => List.fill(stack.length) { BlockEndToken } //closing all opened blocks
case line :: rest => {
(computeIndentation(line), stack) match {
case (indentation, top :: stackRest) if indentation > top => {
BlockStartToken :: line :: insertTokens(rest, indentation :: stack)
}
case (indentation, top :: stackRest) if indentation == top =>
line :: insertTokens(rest, stack)
case (indentation, top :: stackRest) if indentation < top => {
BlockEndToken :: insertTokens(lines, stackRest)
}
case _ => throw new IllegalStateException("Invalid algorithm")
}
}
}
private def computeIndentation(line: String): Int = {
val whiteSpace = line takeWhile isWhiteChar
(whiteSpace map {
case ' ' => 1
case '\t' => TabSize
}).sum
}
private def isWhiteChar(ch: Char) = ch == ' ' || ch == '\t'
}
Wykonanie tego tekstu otrzymujemy:
val text =
"""
|line1
|line2
| line3
| line4
| line5
| line6
| line7
| line8
| line9
|line10
| line11
| line12
| line13
""".stripMargin
println(Preprocessor.preProcess(text))
... następujący wynik
{
line1
line2
{
line3
line4
line5
{
line6
line7
}
}
{
line8
line9
}
line10
{
line11
line12
line13
}
}
I Afterwords można użyj biblioteki kombinatorów, aby wykonać parsowanie w prostszy sposób.
Mam nadzieję, że to pomoże
Użyj 'override val skipWhitespace = false' – senia