parboiled2генерируемые макросами генераторы PEG-парсеров
Александр Мыльцев / / github linkedin.com/in/alexandermyltsevпрезентация
задача парсингавход: строка, поток байт, ...выход: обладает структурой (да/нет), структурапример
1 + 2 * 3 — арифметическое выражение: да или нет? — да
1 + 2 * 3 — структура? — Plus(1, Mul(2, 3))
регулярные выражениянельзя: рекурсивные структуры
1. калькулятор2. json3. ...
рекурсивный нисходящий парсер:C/C++
static int lex_scan_number(lex_t *lex, int c, json_error_t *error){ const char *saved_text; char *end; double doubleval;
lex->token = TOKEN_INVALID;
if(c == '-') c = lex_get_save(lex, error);
if(c == '0') { c = lex_get_save(lex, error); if(l_isdigit(c)) { lex_unget_unsave(lex, c); goto out; } } else if(l_isdigit(c)) { c = lex_get_save(lex, error); while(l_isdigit(c)) c = lex_get_save(lex, error); } else { lex_unget_unsave(lex, c); goto out; }
if(c != '.' && c != 'E' && c != 'e') { json_int_t intval;
lex_unget_unsave(lex, c);
saved_text = strbuffer_value(&lex->saved_text);
https://github.com/akheron/jansson/blob/master/src/load.c
рекурсивный нисходящий парсер:JavaScript
number = function () {
// Parse a number value.
var number, string = '';
if (ch === '-') { string = '-'; next('-'); } while (ch >= '0' && ch <= '9') { string += ch; next(); } if (ch === '.') { string += '.'; while (next() && ch >= '0' && ch <= '9') { string += ch; } } if (ch === 'e' || ch === 'E') { string += ch; next(); if (ch === '-' || ch === '+') { string += ch; next(); } while (ch >= '0' && ch <= '9') { string += ch; next(); } } number = +string;
https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
рекурсивный нисходящий парсерплюсы
1. код на одном языке2. одна среда разработки3. высокая производительность
минусы1. нет предметно-ориентиррованного языка2. низкоуровневое кодирование
внутренний язык и среда разработки
генерация java-файлов
подключение файлов к проекту
ANTLR3
grammar SimpleCalc; add : NUMBER PLUS NUMBER; NUMBER : ('0'..'9')+ ; PLUS : '+';
java org.antlr.Tool SimpleCalc.g
ANTLR3плюсы
1. предметно-ориентированный язык2. высокая производительность
минусы1. новый язык, другая среда2. генерация и подключение файлов
комбинаторы парсеровHaskell ParsecScala Combinator ParsersFParsec
комбинаторы парсеровплюсы
1. программа на исходном языке2. файлы в исходном проекте3. исходная среда разработки4. типизация парсеров5. близкая к БНФ форма записи
// Expr ::= Term { '+' Term | '-' Term }def expr = term ~ rep( "+" ~ term | "-" ~ term )
// Term ::= Factor { '*' Factor | '/' Factor }def term = factor ~ rep ( "*" ~ factor | "/" ~ factor )
// Factor ::= Number | '(' Expr ')'def factor = number | "(" ~ expr ~ ")"
минусы1. интерпретатор2. временные объекты3. низкая производительность
генератор парсеров1. на одном языке в одной среде разработки2. типизированный DSL3. компиляция, а не интерпритация
parboiled2PEG — грамматика, разбирающая выражениетипизированный DSLкомпиляция правил
rule, ch, strреализация класса `Parser`правила грамматики внутри метода rule { }
class SampleParser(val input: ParserInput) extends Parser { def Abc = rule { "abc" } def Ch = rule { '7' } def Num = rule { 42 } // ошибка компиляции}
a ~ b, a | bclass SampleParser(val input: ParserInput) extends Parser { def r1 = rule { "a" ~ "b" } // "ab" def r2 = rule { "a" | "b" } // { "a", "b" } def r3 = rule { "a" ~ ("b" | "c") } // { "ab", "ac" } def r4 = rule { "ab" | "a" }}
zeroOrMore, oneOrMoreclass SampleParser(val input: ParserInput) extends Parser { def r1 = rule { zeroOrMore("a") } // { "", "a", "aa", "aaa", ... } def r2 = rule { oneOrMore("a") } // { "a", "aa", "aaa", ... }}
парсер арифметическихвыражений
class CalculatorParser(val input: ParserInput) extends Parser { def InputLine = rule { Expression ~ EOI }
// Рекурсивный вызов в Parens, поэтому нужен тип def Expression: Rule0 = rule { Term ~ zeroOrMore('+' ~ Term | '-' ~ Term) }
def Term = rule { Factor ~ zeroOrMore('*' ~ Factor | '/' ~ Factor) }
def Factor = rule { Number | Parens }
def Parens = rule { '(' ~ Expression ~ ')' }
def Number = rule { oneOrMore(CharPredicate.Digit) }}
арифметическое выражение:да или нет?
val line = readline()val calc = new CalculatorParser(line)
calc.InputLine().run() match { case Success(_) => "Yes" case Failure(_) => "No"}
parser actions, value-stackvalue-stack — отличие от haskell parsec et al.
class ValueStack(initialSize: Int, maxSize: Int) extends Iterable[Any] { var buffer = new Array[Any](initialSize) var size = 0}
def Digits = rule { oneOrMore("0" | "1" | /*...*/ "9") } // oneOrMore("0"-"9")
def False = rule { "false" ~ push(false) } // "false" >>> VStk: [false]
def NumberStr = rule { capture(Digits) } // "42" >>> VStk: ["42"]
def Number = rule { capture(Digits) ~> (_.toInt) } "42" >>> // VStk: [42]
def Sum = rule { Number ~ " " ~ Number ~> ((_:Int) + _) } // "11 15" >>> VStk: [26]
калькуляторclass CalculatorParser(val input: ParserInput) extends Parser { def Expression: Rule1[Int] = rule { Term ~ zeroOrMore( '+' ~ Term ~> ((_: Int) + _) | '-' ~ Term ~> ((_: Int) - _)) }
def Term = rule { Factor ~ zeroOrMore( '*' ~ Factor ~> ((_: Int) * _) | '/' ~ Factor ~> ((_: Int) / _)) }}
`~>` может передавать элемент AST в стек`~>`-обработчик сам может быть парсером
типизация правил и value-stackdef InputRule: Rule[HNil, String :: HNil] = rule { capture("a") }
class Rule[-I <: HList, +O <: HList]I - тип значения со стекаO - тип значения в стек
type RuleN[L <: HList] = Rule[HNil, L]type Rule0 = RuleN[HNil]type Rule1[T] = RuleN[T :: HNil]type Rule2[A, B] = RuleN[A :: B :: HNil]
типизация правил и value-stackdef a: Rule1[String] = rule { capture("a") }
def b: Rule1[String] = rule { capture("7") }
def bi: Rule1[Int] = rule { b ~> ((_:String).toInt) }
типизация правил и value-stackdef c1: Rule2[String, Int] = rule { a ~ bi }
def c2: Rule2[Int, Int] = rule { bi ~ push(42) }
def c3: Rule[Int :: HNil, Int :: HNil] = rule { capture("7") ~> ((x: Int, y: String) => x + y.toInt) }
компиляцияabstract class Parser { def rule(r: Rule): Rule = macro Parser.ruleImpl}
object Parser { def ruleImpl(ctx: ParserContext)(r: ctx.Expr[Rule]): ctx.Expr[Rule] = reify { OpTree(r.tree).render().splice }}
компиляцияobject OpTree { def apply(tree: Tree): OpTree = r.tree match { // квазицитирования case q"$a.this.str($s)" => LiteralString(s) case q"$a.this.ch($ch)" => LiteralChar(ch) case q"$lhs.|($rhs)" => FirstOf(OpTree(lhs), OpTree(rhs)) case q"$lhs.~($rhs)" => Sequence(OpTree(lhs), OpTree(rhs)) }}
abstract class OpTree { def render(): Expr[Rule]}
case class Rule(val matched: Boolean)
LiteralCharimplicit def ch(c: Char): Rule0rule { ch('a') }case q"$a.this.ch($ch)" => // $a = CalculatorParser, $ch = CharLiteral('a')
case class LiteralChar(charTree: Tree) extends OpTree { def render(): Expr[Rule] = reify { val char = c.Expr[Char](charTree).splice val p = c.prefix.splice // Current parser Rule(p.__nextChar() == char) }}
LiteralStringimplicit def str(c: String): Rule0rule { str("abc") }case q"$a.this.str($s)" => // $a = CalculatorParser, $s = StringLiteral("abc")
case class LiteralString(s: Tree) extends OpTree { def render(): Expr[Rule] = c.Expr[Rule](q̀ val p = ${c.prefix} // Current parser var ix = 0 while (ix < $s.length && $s.charAt(ix) == p.__nextChar()) { ix += 1 } Rule(ix == $s.length))̀
FirstOf: a | b// case q"$lhs.|($rhs)" =>
case class FirstOf(charTree: Tree) extends OpTree { def render(): Expr[Rule] = c.Expr[Rule](q̀ val mark = p.__cursor if (${OpTree(lhs).render()}.matched) { Rule(true) } else { p.__cursor = mark ${OpTree(rhs).render()} } ̀)}
Sequence: a ~ b// case q"$lhs.~($rhs)" =>
case class FirstOf(charTree: Tree) extends OpTree { def render(): Expr[Rule] = c.Expr[Rule](q̀ Rule(${OpTree(lhs).render()}.matched && ${OpTree(rhs).render()}.matched) ̀)}
parboiled2.orgoneOrMorezeroOrMore~>...
использованиеjson-парсер в ~100 быстрее Scala Parsers, в ~10 parboiled1typesafe, spray.io, spray-json
parboiled2.orglinkedin.com/in/alexandermyltsev
вопросы