2016-08-05 4 views
2

Я работал над проектом, связанным с парсером, и я его реализовал с использованием recursive descent parser. Однако проблема может привести к переполнению стека. Каковы методы решения этой проблемы?избежать stackoverflow в рекурсивном алгоритме в парсере рекурсивного спуска

Для иллюстрации, это простой синтаксический анализатор математического выражения, с добавлением, вычитанием, умножением и поддержкой разделения. Можно использовать группирующие скобки, и они, очевидно, вызывают рекурсию.

вот полный код:

#include <string> 
#include <list> 
#include <iostream> 

using namespace std; 

struct term_t; 
typedef list<term_t> prod_t; 
typedef list<prod_t> expr_t; 
struct term_t 
{ 
    bool div; 
    double value; 
    expr_t expr; 
}; 

double eval(const expr_t &expr); 
double eval(const term_t &term) 
{ 
    return !term.expr.empty() ? eval(term.expr) : term.value; 
} 
double eval(const prod_t &terms) 
{ 
    double ret = 1; 
    for (const auto &term : terms) 
    { 
     double x = eval(term); 
     if (term.div) 
      ret /= x; 
     else 
      ret *= x; 
    } 
    return ret; 
} 
double eval(const expr_t &expr) 
{ 
    double ret = 0; 
    for (const auto &prod : expr) 
     ret += eval(prod); 
    return ret; 
} 

class expression 
{ 
public: 
    expression(const char *expr) : p(expr) 
    { 
     prod(); 
     for (;;) 
     { 
      ws(); 
      if (!next('+') && *p != '-') // treat (a-b) as (a+-b) 
       break; 
      prod(); 
     } 
    } 
    operator const expr_t&() const 
    { 
     return expr; 
    } 

private: 
    void term() 
    { 
     expr.back().resize(expr.back().size() + 1); 
     term_t &t = expr.back().back(); 
     ws(); 
     if (next('(')) 
     { 
      expression parser(p); // recursion 
      p = parser.p; 
      t.expr.swap(parser.expr); 
      ws(); 
      if (!next(')')) 
       throw "expected ')'"; 
     } 
     else 
      num(t.value); 
    } 
    void num(double &f) 
    { 
     int n; 
     if (sscanf(p, "%lf%n", &f, &n) < 1) 
      throw "cannot parse number"; 
     p += n; 
    } 
    void prod() 
    { 
     expr.resize(expr.size() + 1); 
     term(); 
     for (;;) 
     { 
      ws(); 
      if (!next('/') && !next('*')) 
       break; 
      term(); 
     } 
    } 
    void ws() 
    { 
     while (*p == ' ' || *p == '\t') 
      ++p; 
    } 
    bool next(char c) 
    { 
     if (*p != c) 
      return false; 
     ++p; 
     return true; 
    } 

    const char *p; 
    expr_t expr; 
}; 

int main() 
{ 
    string expr; 
    while (getline(cin, expr)) 
     cout << "= " << eval(expression(expr.c_str())) << endl; 
} 

если вы бежите, вы можете ввести простые математические выражения, как 1+2*3+4*(5+6*7) и правильно рассчитать 195. Я также добавил оценку простого выражения, это также вызывает рекурсию и вызывает переполнение стека даже проще, чем синтаксический анализ. В любом случае, синтаксический анализ является простым и очевидным, как я могу его переписать без внесения больших изменений в код и полностью избежать рекурсии? В моем случае я использую выражение, подобное этому (((((1))))), чтобы вызвать рекурсию, и если у меня будет всего несколько сотен круглых скобок, я получаю переполнение стека. Если я пройду через дерево рекурсии отладчика (в Visual Studio), если только три функции: [term ->] expression ctor ->prod ->term и из проверки регистра эти три функции занимают 700-1000 байт пространства стека. С настройками оптимизации и программным кодом немного могу сделать это меньше, а с настройками компилятора я могу увеличить пространство стека, или в этом случае я мог бы также использовать Dijksta's shunting-yard algorithm, но это не вопрос: я хочу знать, как переписать чтобы избежать рекурсии и в то же время, если это возможно, без полного переписывания кода анализа.

+2

посмотреть на «оптимизацию хвостовой рекурсии». Основная идея состоит в том, чтобы передать частичные результаты в рекурсивную функцию. Затем компилятор будет знать, что повторно использует один и тот же фрейм для каждого рекурсивного вызова – DanielS

+0

* если у меня всего несколько сотен круглых скобок * - В реальном сценарии вы действительно ожидаете 200+ вложенных круглых скобок? – PaulMcKenzie

+0

@PaulMcKenzie в реальной жизни У меня не было этой проблемы, но я хотел выяснить для себя, как обращаться с такими вещами. – Pavel

ответ

3

A рекурсивный -размерный парсер обязательно рекурсивный; имя не капризно.

Если произведение является право-рекурсивным, то соответствующее ему рекурсивное спускательное действие является хвостовым рекурсивным. Таким образом, с соответствующей грамматикой вы можете создать хвостовой рекурсивный синтаксический анализатор, но производство для выражений в скобках будет затруднено в этом ограничении. (И см. Ниже.)

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

ПРИМЕЧАНИЕ: Если вы используете C++, вам нужно пройти через некоторые обручи, чтобы создать контекст хвоста. В частности, если вы выделяете объект с нетривиальным деструктором (например, std :: list), тогда вызов автоматического деструктора происходит в хвостовом контексте, а последний явный вызов функции не является хвостовым вызовом.

+1

, прежде чем задавать вопрос, я действительно экспериментировал с рекурсией хвоста, и это кажется проблематичным на C++, даже если вы явно не выделяете нетривиальные объекты в стеке. Мне пришлось прибегать ко всем видам причуд, чтобы уменьшить использование стека. – Pavel

+0

Кроме того, я реализовал этот простой парсер без рекурсии, однако другие рекурсивные функции вызовут stackoverflow. Есть две, очевидно, рекурсивные функции: синтаксический анализ и 'eval', даже после того, как я получил их измененный деструктор, будет вызвано переполнение стека :), и очевидно, что он должен это сделать, если вы проверите структуру' expr_t' – Pavel

+0

@Pavel: вы можете конечно разобрать без рекурсии. Но для этого требуется переписывать парсер, который, как вы говорите, выходит за рамки.Лично я бы предложил использовать дерево двоичных выражений, а не списки списков списков; у списков есть больше накладных расходов, чем вы, вероятно, хотите, и не покупаете вас много, если угодно, с точки зрения простоты кода. – rici

1

Для разбора выражений взгляните на синтаксический анализ приоритета оператора, например http://epaperpress.com/oper/download/OperatorPrecedenceParsing.pdf. Он анализирует выражения в простом цикле, используя стек данных. Единственное пространство, необходимое для 200 вложенных скобок, - 200 записей в стеке данных.

Есть языки, где новые операторы могут быть добавлены во время выполнения, с компилируемой программой, определяющей ассоциативность и приоритет этих операторов, что не может быть обработано с помощью рекурсивного приличного парсера.

+0

, но шунтирующий год делает именно это. Описывает ли этот документ этот алго? – Pavel

+0

, который выглядит как алгоритм шунтирования, но он не упоминает об этом. – Pavel

+0

Приоритет оператора довольно прост для реализации как часть рекурсивного анализатора спуска. Один из примеров - часть учебника llvm http://llvm.org/docs/tutorial/LangImpl02.html –

2

Общей практикой для парсеров рекурсивного спуска является рекурсия в подвыражения, нетерминалы или вложенные конструкции, но не для использования рекурсии для продолжения разбора на одном уровне. Это делает размер стека предельным для максимальной «глубины» строки, которую вы можете проанализировать, но не по ее длине.

Похоже, что вы сделали, что часть права, так что давайте посмотрим на типичные числа ...

Из-за ограничений на основе стека, рекурсивные функции синтаксического анализа, как правило, написаны так, что они не использовать много стека - 128 байтов или около того было бы высоким средним.

Итак, если у вас есть, скажем, 128 Кбайт пространства стека (что обычно означает, что ваш стек уже заполнен на 90%), то вы должны иметь возможность получить 1000 уровней или около того, и это много для реального мира тексты, которые программисты на самом деле печатают.

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

Я не знаю размер всего класса, но я бы предположил, что основная проблема заключается в term(), где вы помещаете целое новое expression в стек с объявлением expression parser(p);. Это очень необычно и выглядит так, что это может занимать много места. Вероятно, вам следует избегать создания всего этого нового объекта.

Распечатайте sizeof(expression), чтобы увидеть, насколько велика эта на самом деле.

+0

в моем фактическом коде. Я использовал unitque_ptr, потому что выражение было больше. В этом примере кода он должен быть небольшим: это указатель и список; список, вероятно, является указателем и размером внутри. Итак, 12-16 байтов в 32-битном режиме – Pavel

Смежные вопросы