PEG + Pratt = простота, скорость и расширяемость парсера
От: VladD2 Российская Империя www.nemerle.org
Дата: 09.02.11 05:48
Оценка:
Предлагаю обсудить очень простую, но на мой взгляд очень перспективную мысль соединение PEG п Pratt парсеров.

Начальные данные (если в обсуждение влезут те кто не в теме и забанен в гугле):
PEG
Автор: Чистяков Владислав Юрьевич
Дата: 11.12.10
(в начале статьи дается общее описание)
Pratt

Преимущества PEG-а:
1. Безлексерный.
2. Дико мощный. Парсит все что шевелится, остальное сначала раскачивает, а потом парсит.
3. В нашей реализации довольно шустрый.
4. Может расширяться за счет превращения правил приоритетного выбора в массивы с динамическим их заполнением.

Недостатки PEG-а:
1. Отсутствует левая рекурсия. В следствии этого разбор операторов требует некоторых приседаний. В частности не обеспечивает декларативного разгуливания ассоциативности. Приоритеты могут разруливаться путем организации иерархии правил, но это приводит к усложнению грамматики, что плохо как для ее понимания, так и для скорости работы парсера.
2. В следствии крутости своих откатов несколько усложняет выявление мест ошибок.

Преимущества Pratt-а:
1. Очень эффективен.
2. Нереально прост, если речь идет о разборе бинарных и унарных операторов.
3. Легко создать динамически расширяемую версию.
4. Позволяет легко выявлять ошибки и формировать качественные диагностические сообщения.
5. Позволяет описывать операторы в полностью декларативном стиле. Все что нужно — задать имя оператора, правый и левый приоритеты, а так же тип оператора (префиксный, посфиксный или инфиксный).

Недостатки Pratt-а:
1. Для разбора операторов не являющихся бинарными или унарными (например, тернарными) требует определенных приседаний.
2. Практически не пригоден для разбора грамматик не относящихся к операторам.
4. Лексерный, т.е. требует предварительной разбивки текста на токены.

Теперь давайте подытожим сказанное выше и попробуем выделить суть.

Итак, PEG хорош когда речь идет о разборе чего угодно, но не выражений (expressions). Pratt хорош в только для разбора выражений и практически не пригоден для разбора чего-либо другого.

Сама собой напрашивается идея объединить их и заставить PEG разбирать все что не связано с синтаксисом выражений, а Pratt наоборот только для разбора выражений.

Однако PEG безлексерный, а Pratt лексерный. Как же их совместить вместе?

Как известно PEG — это отличная замена регулярным ываржениям. А от регулярных выражений до лексера рукой подать. Так что можно попробовать заставить PEG работать в режиме лексера в случае если он обнаружил нечто похожее на выражения.

Но что значит "обнаружил нечто похожее на выражения"? Как определить выражения не проиводя синтаксический анализ?

Другими словами, что отличает выражения от любой другой грамматики? А на самом деле очень просто. Выражение есть ни что иное как набор простых выражений разделенных операторами.

Что значит простые выражения? Очень просто. Это примитивные выражения вроде идентификаторов (например: a, Test, _x), литералов (например: 42, "мама"), и других выражений заключенных в скобки. На самом деле все зависит от языка и к простым выражениям можно отнести и другие выражения, но это уже не важно. Важно, что в любом языке есть простые выражения и составные выражения состоящие из простых выражений разделенных операторами. Так вот простое выражение можно выразить как одно PEG-правило — simpleExpr.

Остается разобраться что же такое оператор. Как и простое выражение все многообразие операторов можно выразить отдельным правилом PEG. Это правило может состоять из списка отдельных операторов, или тупо считать оператором неразрывную последовательность операторных символов (например: ~!#$%^&*_+?/\:.). Такое правило можно назвать operatorRef.

Имея два таких правила — operatorRef и simpleExpr можно легко сформулировать PEG-грамматику выделяющую выражения и превращающую ее в поток токенов. Причем так как simpleExpr может содержать выражения в скобках, поток будет иерархическим.

Вот как может выглядеть PEG-правило предварительного разбора операторных токенов (грамматика в формате Nw=emerle.Peg):
expr : List[Token] = simpleExpr? (operatorRef+ simpleExpr)* operatorRef*;


Так как в выражениях могут встречаться, а могут и не встречаться унарные операторы (префиксные или постфиксные), то в грамматике необходимо предусмотреть как случай когда они есть, и когда их нет. В стальном грамматика просто распознает набор simpleExpr разделенных одним или более оператором (operatorRef).

Обработчик этого правила незамысловат:
expr(exprOpt : option[Token], instrs : List[List[string] * Token], tailOps : List[string]) : List[Token]
{
  def res = List();
  
  when (exprOpt is Some(expr))
    res.Add(expr);
  
  foreach ((operators, expr) in instrs)
  {
    foreach (op in operators)
      res.Add(Token.Operator(op));
      
    res.Add(expr);
  }

  foreach (op in tailOps)
    res.Add(Token.Operator(op));
    
  res
}

Вся его суть заключается в том, что он собирает токены в список и возвращает этот список.
Этот список может быть пустым. Если он не пуст, то он обязательно содержит хотя бы один токен сформированный правилом simpleExpr и неограниченное количество операторов (токенов распознанных правилом operatorRef). Если в списки встречается более одного токена разобранного правилом simpleExpr, то между такими токенами обязательно встречается хотя бы один операторный токен.

Такой набор токенов можно "скормить" Pratt-парсеру и получить от него результирующее выражение.

В чем навар?

Выгоды очевидны. Хотя PEG используется в данном подходе только как продвинутый лексер, но все же во-первых он позволяет очень гибко интерпретировать понятие simpleExpr и operatorRef. Так как они остаются правилами PEG-грамматики их можно расширять динамически просто сделав динамически формируемый оператор приоритетного выбора (то что в Немерле 2 называется точками расширения).

Это позволяет нам, например, добавить в правило simpleExpr более сложные операторы которые сложнее описать средствами Pratt-парсера. Например, мы можем создать PEG-правило описывающее оператор if:
ifExpr : Token = "if"s openBracket expr closeBracket expr "else"s expr;

и добавить его к определению правила simpleExpr:
simpleExpr : Token = ifExpr / num / parenthesesExpr / ident;

и сможем использовать в выражениях оператор "if (...) ... else ...".

Таким образом список унарных и бинарных операторов можно расширять за счет возможностей Pratt-парсера. При этом мы можем пользоваться всеми его преимуществами и объявлять операторы декларативное. И в то же время мы можем встраивать более сложные синтаксические конструкции за счет возможностей PEG-а.

Вместо заключения

Приняв на себя роль советских ученых, я провел смелый научный эксперимент и реализовал прототип подобного парсера. В нем все "приклеено гвоздями". Нет никакой расширяемости и вообще намека на реальное использование. Но его цель не в этом. Он демонстрирует возможность совмещения двух подходов в одном алгоритме.

Просьба оценить мысль и покритиковать. Может быть я что-то упустил?

Исходники

Код проекта на Nemerle можно скачать здесь.

Ниже я привожу этот код для тех кто кому интересно что у меня получилось, но он не хочет качать, компилировать запускать код (хотя это и намного интереснее).

В двух словах о приложении. Оно позволяет ввести в командную строку выражение, после чего разбирает его и выводит текстовое представление полученного АСТ на консоль.

Main.n (консольный интерфейс)
  Скрытый текст
using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Console;
using System.Linq;
using ExprParser;
using Nemerle.Peg;

module Program
{
  Main() : void
  {
    def parser = ExprParser();
    mutable text = "2 + -5";
    
    WriteLine(text);
    do
    {
      match (parser.Parse(text))
      {
        | Some(tokens) =>
          def parser = Pratt();
          def ast    = parser.Parse(tokens);
          WriteLine(ast);
          
        | None => 
          def (pos, rules) = parser.GetMaxRollbackPosAndNames();
          WriteLine(string(' ', pos) + "^");
          WriteLine($"Expected: ..$rules");
      }
      
      text = Console.ReadLine();
    }
    while (text != "");
  }
}

Token.n — описание токена. Используется для передачи данных от PEG-парсера к Pratt-парсеру.
  Скрытый текст
using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Linq;

namespace ExprParser
{
  public variant Token
  {
    | Ident          { Name : string; }
    | Num            { Value : int; }
    | Operator       { Text : string; }
    | Parentheses    { Children : List[Token]; }
    | Expr           { Value : PExpr; }
    | Eof
    
    public override ToString() : string
    {
      match (this)
      {
        | Ident       (name)     => $"'$name'"
        | Num         (value)    => $"'$value'"
        | Parentheses (children) => $"(..$children)"
        | Operator    (text)     => $"'$text'"
        | Expr        (value)    => value.ToString()
        | Eof                    => <#"EOF"#>
      }
    }
  }
}

ExprParser.n — PEG-парсер (на оснвое Nemerle.Peg).
  Скрытый текст
using Nemerle.Collections;
using Nemerle.Peg;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Linq;

namespace ExprParser
{
  [PegGrammar(
    start,
    grammar {
      any                     = ['\u0000'..'\uFFFF'];
      start : List[Token]     = expr !any;
      //semicolon : NToken      = ";"s;
      //semicolonOpt            = (";"s)?;

      #region Line terminators

      newLineCharacter = '\n'
                        / '\r'
                        / '\u2028'    /*  line separator       */
                        / '\u2029';   /*  paragraph separator  */
      newLine   = "\r\n" / newLineCharacter;

      #endregion

      #region White space

      whitespace = [Zs]
                / '\t'
                / '\v'        /*  vertial tab          */
                / '\f';       /*  form feed            */

      #endregion

      #region Spacer

      space = whitespace / newLine;

      [InlineAllSubrules]
      s : void = space*;                      /* optional spacer          */

      #endregion
      
      inentStartChar = ['A'..'Z', 'a'..'z'] / '_';
      identPartChar  = inentStartChar / ['0'..'9'];
      identCars      = inentStartChar identPartChar*;
      operatorChar = '+' / '-' / '\\' / '*' / '.' / '!' / '~' / '<' / '>' / '=' / '&' / '|' / '^' / '%' / '#' / '$' / '/';
      openBracket     : NToken = "("s;
      closeBracket    : NToken = ")"s;

      ifExpr          : Token       = "if"s openBracket expr closeBracket expr "else"s expr;

      num             : Token       = ['0'..'9']+ s;
      ident           : Token       = identCars s;
      operatorRef     : string      = operatorChar+ s;
      simpleExpr      : Token       = ifExpr / num / parenthesesExpr / ident;
      parenthesesExpr : Token       = openBracket expr closeBracket;
      expr            : List[Token] = simpleExpr? (operatorRef+ simpleExpr)* operatorRef*;
    }
  )]
  class ExprParser
  {
    operatorRef(op : NToken) : string
    {
      GetText(op)
    }
    ident(text : NToken) : Token
    {
      Token.Ident(GetText(text))
    }
    num(text : NToken) : Token
    {
      Token.Num(int.Parse(GetText(text)))
    }
    
    parenthesesExpr(_ : NToken, expr : List[Token], _ : NToken) : Token
    {
      //Token.Parentheses(expr)
      def parser = Pratt();
      def res = parser.Parse(expr);
      Token.Expr(res)
    }
    
    expr(exprOpt : option[Token], instrs : List[List[string] * Token], tailOps : List[string]) : List[Token]
    {
      def res = List();
      
      when (exprOpt is Some(expr))
        res.Add(expr);
      
      foreach ((operators, expr) in instrs)
      {
        foreach (op in operators)
          res.Add(Token.Operator(op));
          
        res.Add(expr);
      }

      foreach (op in tailOps)
        res.Add(Token.Operator(op));
        
      res
    }
    
    ifExpr(_ : NToken, _ : NToken, condTokens : List[Token], _ : NToken, e1Tokens : List[Token], _ : NToken, e2Tokens : List[Token]) : Token
    {
      def parser = Pratt();
      def cond = parser.Parse(condTokens);
      def e1   = parser.Parse(e1Tokens);
      def e2   = parser.Parse(e2Tokens);
      Token.Expr(PExpr.If(cond, e1, e2))
    }
  }
}

PExpr.n — описание AST выражения. Используется для представления выражений.
  Скрытый текст
using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Linq;

namespace ExprParser
{
  public variant PExpr
  {
    | BinOperator     { LeftExpr : PExpr; Op : string; RightExpr : PExpr; }
    | PrefixOperator  { Op : string; Expr : PExpr; }
    | PostfixOperator { Op : string; Expr : PExpr; }
    | Ident           { Name : string; }
    | Num             { Value : int; }
    | If              { Cond : PExpr; Expr1 : PExpr; Expr2 : PExpr; }
    | Error           { Msg : string; }

    public override ToString() : string
    {
      match (this)
      {
        | BinOperator    (leftExpr, op, rightExpr) => $"$op($leftExpr, $rightExpr)"
        | If             (cond, e1, e2)            => $"o:if($cond, $e1, $e2)"
        | PrefixOperator (op, expr)                => $"p:$op($expr)"
        | PostfixOperator(op, expr)                => $"s:$op($expr)"
        | Ident          (name)                    => $"$name"
        | Num            (value)                   => $"$value"
        | Error          (msg)                     => $"'Error: $msg'"
      }
    }
    
  }
}

Pratt.n — Pratt-парсер. Принимает на вход список токенов и возвращет PExpr (AST).
  Скрытый текст
using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Console;
using System.Collections.Generic;
using System.Linq;

namespace ExprParser
{
  public class Pratt
  {
    public Parse(tokens : List[Token]) : PExpr
    {
      mutable currentIndex = 0;
      def getLeftBindingPower(tok : Token, prefix : bool) : int
      {
        match (tok)
        { // это надо формировать динамически
          | Ident                 => 0
          | Num                   => 0
          | Expr                  => 0
          | Operator   ("&&")     => 30
          | Operator   ("||")     => 30
          | Operator   ("+")      => 50
          | Operator   ("-")      => if (prefix) 100 else 50
          | Operator   ("*")      => 60
          | Operator   ("/")      => 60
          | Operator   (".")      => 80
          | Operator   (_)        => assert(false)
          | Parentheses           => 0
          | Eof                   => int.MinValue
        }
      }
      def next()
      {
        if (currentIndex < tokens.Count)
        {
          def tok = tokens[currentIndex];
          ++currentIndex;
          tok
        }
        else
          Token.Eof()
      }
      
      mutable token = next();
      
      def expression(rbp) : PExpr
      {
        mutable t     = token;
                token = next();
        mutable left  = nud(t);
        
        while (rbp < getLeftBindingPower(token, false))
        {
          t = token;
          token = next();
          left = led(left, t);
        }
        
        left;
      }      
      and nud(tok : Token) : PExpr
      {
        | Ident      (name)     => PExpr.Ident(name)
        | Num        (value)    => PExpr.Num(value)
        | Operator   ("-")      => PExpr.PrefixOperator("-", expression(getLeftBindingPower(tok, true)))
        | Operator   (_)        => assert(false)
        | Parentheses(children) => Parse(children)
        | Expr(expr)            => expr
        | Eof                   => PExpr.Error("Expected simple expressoin.")
      }
      and led(left : PExpr, tok : Token) : PExpr
      {
        match (tok)
        {
          | Operator   (op) => PExpr.BinOperator(left, op, expression(getLeftBindingPower(tok, false)))
          | _ => assert(false) 
        }
      }
      
      expression(0)
    }
  }
}
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.