2016-11-24 7 views
1

Итак, вопрос о грамматике ниже. Я работаю над мини-интерпретируемым языком для удовольствия (мы узнали о компиляторе в классе, поэтому я хочу перейти на следующий уровень и попробовать что-то самостоятельно). Я застрял, пытаясь сделать нетерминальный символ Expr.Как изменить грамматику синтаксического анализа, чтобы разрешить присваивание и инструкции присваивания?

Statement ::= Expr SC 
Expr ::=   /* I need help here */ 
Assign ::= Name EQUAL Expr 
AddSub ::= MulDiv {(+|-) AddSub} 
MulDiv ::= Primary {(*|/) MulDiv} 
Primary ::= INT | FLOAT | STR | LP Expr RP | Name 
Name ::= ID {. Name} 

Expr должен быть сделан таким образом, что Statement должна позволять для двух случаев:

  1. x = 789; (регулярное назначение, а затем точкой с запятой)
  2. x+2; (нет присваивания, только вычисление, отбрасывается; затем точка с запятой)

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

Я смотрел на другие грамматики (C# именно), но это было слишком сложно и долго, чтобы понять. Естественно, я не ищу решений, но только для руководства о том, как я могу изменить свою грамматику.

Вся помощь приветствуется.

EDIT: Я должен сказать, что моя первоначальная мысль была Expr ::= Assign | AddSub, но это не сработало бы, так как это создало бы двусмысленность, так как оба могут начинаться с нетерминального символа Name. Я сделал свой токенизатор таким, чтобы он позволял одному токену смотреть вперед (заглядывать), но я не делал такого для не-терминалов, поскольку он пытался исправить проблему, которую можно было бы избежать (двусмысленность). В грамматике терминалы - это те, которые являются все-шапками.

+0

Ваш вопрос отсутствует только одна важная деталь: какую грамматику вы ищете? И какой инструмент вы используете для генерации парсера? –

+0

Ну, я не уверен, какие типы грамматик есть (мы говорили только об EBNF, контекст бесплатно). Для этого я не буду использовать автоматизированный инструмент. – Dave

+0

Хорошо, а какой парсер вы реализуете для своей контекстно-свободной грамматики? –

ответ

2

Простейшим решением является фактически принятое разработчиками C и, следовательно, различными производными C: трактуйте назначение просто как еще один оператор, не ограничивая его тем, что он находится на верхнем уровне инструкции. Таким образом, в C, следующее беспроблемное:

while ((ch = getchar()) != EOF) { ... } 

Не все считают, что хороший стиль, но это, конечно, часто (особенно в положениях о for заявления, синтаксис которых более или менее требует, чтобы назначение быть выражением).

Есть две небольшие осложнения, которые относительно легко выполнить:

  1. Логически, и в отличии от большинства операторов присваивания правоассоциативного так, что a = b = 0 анализируются как a = (b = 0) и не (a = b) = 0 (что было бы очень неожиданно). Он также связывает очень слабо, по крайней мере, вправо.

    Мнения различаются в зависимости от того, насколько сильно он должен связываться слева. В C, по большей части, применяется строгая модель приоритета, так что a = 2 + b = 3 отклоняется, так как он анализируется как a = ((2 + b) = 3). a = 2 + b = 3 может показаться страшным, но рассмотрим также a < b ? (x = a) : (y = a). В C++, где результат тернарного оператора может быть ссылкой, вы можете написать, что как (a < b ? x : y) = a, в котором требуются скобки, даже назначение мысли имеет более низкий приоритет, чем тройной оператор.

    Однако ни один из этих вариантов трудно реализовать в грамматике.

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

Если вы действительно хотите, чтобы различать операторы присваивания из утверждений выражения, то вы действительно столкнулись с проблемой отказа предсказания (не неоднозначности), если вы используете сверху вниз разбор техники, такие как рекурсивный спуск. Поскольку грамматика не является двусмысленной, простым решением является использование генератора парсера LALR (1), такого как bison/yacc, который не имеет проблем с анализом такой грамматики, поскольку он не требует раннего решения относительно того, какой тип утверждения разобран. В целом использование генераторов парсеров LALR (1) или даже GLR упрощает реализацию парсера, позволяя вам указывать грамматику в форме, которая легко читается и соответствует синтаксическому анализу. (Например, парсер LALR (1) может обрабатывать лево-ассоциативные операторы естественным образом, в то время как грамматика LL (1) может производить только право-ассоциативные разборки и поэтому требует какой-то реконструкции дерева синтаксиса.)

A рекурсивный спуск парсер - это компьютерная программа, а не грамматика, и ее выразительность, таким образом, не ограничена формальными ограничениями грамматик LL (1). Это и сила, и слабость: сила в том, что вы можете найти решения, которые не ограничены ограничениями грамматик LL (1); слабость заключается в том, что гораздо сложнее (даже, иногда, невозможно) извлечь ясное утверждение о точном синтаксисе языка. Эта сила, например, позволяет рекурсивным грамматикам спуска обрабатывать левую ассоциативность более или менее естественным образом, несмотря на ограничение, упомянутое выше.

Если вы хотите спуститься по этой дороге, то решение достаточно просто. У вас будет какая-то функция:

/* This function parses and returns a single expression */ 
Node expr() { 
    Node left = value(); 
    while (true) { 
    switch (lookahead) { 
     /* handle each possible operator token. I left out 
     * the detail of handling operator precedence since it's 
     * not relevant here 
     */ 
     case OP_PLUS: { 
     accept(lookahead); 
     left = MakeNode(OP_PLUS, left, value()); 
     break; 
     } 
     /* If no operator found, return the current expression */ 
     default: 
     return left; 
    } 
    } 
} 

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

/* This function parses and returns a single expression 
* after the first value has been parsed. The value must be 
* passed as an argument. 
*/ 
Node expr_rest(Node left) { 
    while (true) { 
    switch (lookahead) { 
     /* handle each possible operator token. I left out 
     * the detail of handling operator precedence since it's 
     * not relevant here 
     */ 
     case OP_PLUS: { 
     accept(lookahead); 
     left = MakeNode(OP_PLUS, left, value()); 
     break; 
     } 
     /* If no operator found, return the current expression */ 
     default: 
     return left; 
    } 
    } 
} 

При том, что в месте, это просто реализовать как expr и stmt:

Node expr() { 
    return expr_rest(value()); 
} 

Node stmt() { 
    /* Check lookahead for statements which start with 
    * a keyword. Omitted for simplicity. 
    */ 

    /* either first value in an expr or target of assignment */ 
    Node left = value(); 

    switch (lookahead) { 
    case OP_ASSIGN: 
     accept(lookahead); 
     return MakeAssignment(left, expr()) 
    } 
    /* Handle += and other mutating assignments if desired */ 
    default: { 
     /* Not an assignment, just an expression */ 
     return MakeExpressionStatement(expr_rest(left)); 
    } 
    } 
}