Создание синтаксического синтаксического анализатора (часть компилятора в C ++) - PullRequest
0 голосов
/ 03 апреля 2020

Я делаю компилятор, написанный на C ++.

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

в настоящее время на этапе, когда у меня есть все входные данные, хранящиеся и классифицированные, готовые проверить синтаксис.

... Я думаю, что у меня есть хороший gr asp теории, но все методы, которые я имею придумать для синтаксического разбора синтаксиса серьезные сбои. Для справки, это структура, которая должна быть способной обрабатывать любые конструкции операторов присваивания или объявления, начиная с обобщенного c оператора:

<Statement> -> <Declarative>
<Declarative> -> <Type> <id>

<Statement> -> <Assign>
<Assign> -> <ID> = <Expression>;

<Expression> -> <Expression> + <Term> | <Expression> - <Term> | <Term>

<Term> -> <Term> * <Factor> | <Term> / <Factor> | <Factor>

<Factor> -> ( <Expression> ) | <ID> | <num> 

<ID> -> id

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

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

и все, что в <angle brackets> являются типами токенов

Метод A: "вся строка" - читает 1-й токен в строке, смотрит вперед, чтобы найти ';' и манипулировать оттуда

  • , способный захватить все <expression>
  • , не сможет индивидуально маркировать токены в порядке
until reach end of file (while loop)
   determine statement type (if)
   if the <type> and <id> are the same, the immediate next step to be taken is the same
      need to determine length of statement to be parsed (find start and end of current 'line')
      in <type> and <id> cases, it should be 'current' to 'index of next ;' (length of statement should be from <type> or <id> to the next ';')
   if <type>, should be easy - target format is <type> <id>;
      if token pattern matches that, then is valid, and is noted as such in output
   if <id>, more complex, is assignment
      if token 0 is <id>, token 1 (=) and token 'index' (;) should be the same every time
         between 1 and 'index' is the <expression> to evaluate

... и я не знаю, как я могу оценить часть <expression>, учитывая, что это должна быть какая-то рекурсия

Метод B: "сохранить состояние" - Функция получает входные данные, в которых указывается, какое предыдущее «состояние» оператора было

  • , которое можно вычислить токен за токеном
  • ... но я не знаю не знаю, как я собираюсь оценить <expression>, не заглядывая вперед, чтобы проверить, есть ли токен оператора, который является большим делом всего упражнения
until reach end of file (while loop)
   get passed int to represent previous state, determine current token
      if 0, the statement is starting
         if <type>, expect to be a declaration statement
            return 1
         else if <id>, expect assignment statement
            return 3
      if 1, <type> found previously
         if current token is <id>
            return 2
      if 2, <type> <id> found previously
         if current token is ';'
            return 0, statement complete
      if 3, <id> found previously
         if current token '='
            return 4
      if 4, <id> = found previously
         expression needs evaluation, expect rat's nest of code here

Это все, что у меня есть.

Приложение: Метод C, идея, которая у меня была, только сейчас, которая выглядит очень грязной:

Добавление чего-либо как 4 разные функции, которые могут вызывать друг друга:

function 1 - to read/determine if it's a declaration or assignment statement
function 2 - to determine if an Expression is addition, subtraction, or a Term
function 3 - to determine if a Term is multiplication, division, or a Factor
function 4 - to determine if a Factor is an expression but in parenthesis, an identifier, or a number

Этот последний метод выглядит очень уязвимым для из-за утечки памяти и памяти, но также кажется, что легче выявлять проблемы.

1 Ответ

1 голос
/ 04 апреля 2020

Как уже указывалось в комментариях, ваш метод C известен как рекурсивный спуск и является путем к go, но не может обрабатывать левую рекурсию. Так как с этим справиться?

Вы можете переписать свою грамматику, чтобы удалить левую рекурсию, и у вас получится что-то вроде этого:

<Expression>     -> <Term> <ExpressionTail>
<ExpressionTail> -> + <Term> <ExpressionTail> | - <Term> <ExpressionTail> | ε

Если вы прямо перевели это в код вы бы получили что-то вроде:

Expression* parseExpression() {
    Expression* operand = parseTerm();
    ExpressionTail* tail = parseExpressionTail();
    return new Expression(operand, tail);
}

ExpressionTail* parseExpressionTail() {
    if (current token is '+' or '-') {
        char operator = current token;
        move to next token;
        Expression* operand = parseTerm();
        ExpressionTail* tail = parseExpressionTail();
        return new OperatorExpressionTail(operator, operand, tail);
    } else {
        return new EmptyExpressionTail();
    }
}

Однако есть несколько проблем с этим:

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

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

Для этого мы могли бы заменить return new Expression(operand, tail) кодом, который перебирает хвост и строит из этого правильное дерево выражений. Или мы могли бы полностью избавиться от структуры ExpressionTail и сгенерировать это дерево непосредственно внутри parseExpressionTail, передав левый операнд в качестве аргумента:

Expression* parseExpression() {
    Expression* operand = parseTerm();
    return parseExpressionTail(operand);
}

Expression* parseExpressionTail(Expression* leftOperand) {
    if (current token is '+' or '-') {
        char operator = current token;
        move to next token;
        Expression* rightOperand = parseTerm();
        Expression* newLeftOperand = new InfixExpression(operator, leftOperand, rightOperand);
        return parseExpressionTail(newLeftOperand);
    } else {
        return leftOperand;
    }
}

Теперь получится дерево, которое нам нужно , но это все еще не особенно приятно читать. Вы можете заметить, что parseExpressionTail теперь является хвост-рекурсивным, что означает, что его можно легко переписать как al oop. Как только мы это сделаем, функция больше не будет напрямую рекурсивной, поэтому ее можно вставить в parseExpression. Итак, давайте сделаем это:

Expression* parseExpression() {
    Expression* leftOperand = parseTerm();
    while (current token is '+' or '-') {
        char operator = current token;
        move to next token;
        Expression* rightOperand = parseTerm();
        expression = new InfixExpression(operator, leftOperand, rightOperand);
    }
    return leftOperand;
}

Если мы оглянемся на грамматику, мы могли бы заметить, что expressionTail лучше всего можно описать как «соответствует шаблону (+ | -) <Term> ноль или более раз». Поэтому, если мы введем оператор повторения (например, * в регулярных выражениях или {}, используемый в EBNF) в нашу грамматическую нотацию, мы можем переписать его как

<Expression> -> <Term> ((+ | -) <Term>)*

или

<Expression> -> <Term> {(+ | -) <Term>}

в зависимости от того, какую запись вы предпочитаете. Теперь, если вы пишете свою грамматику с использованием этой нотации в первую очередь, вы можете придумать приведенный выше код более прямым способом, чем сначала записав его рекурсивно, а затем переписав хвостовую рекурсию: вы можете написать свою грамматику, используя операторы повторения где бы это ни имело смысл, а затем просто используйте while l oop везде, где вы видите оператор повторения при переводе грамматики в код.

Теперь мы нашли совершенно выполнимый способ анализа выражений инфиксов в синтаксический анализатор с рекурсивным спуском, но наличие отдельного метода parseFoo для каждого уровня приоритета может все еще стать раздражающим, если у вас много уровней приоритета, и то же самое касается наличия нетерминала в грамматике для каждого уровня приоритета. На уровне грамматики мы можем решить эту проблему, просто записав грамматику двусмысленно, как это:

<Expression> -> <Expression> + <Expression>
              | <Expression> - <Expression>
              | <Expression> * <Expression>
              | <Expression> / <Expression>
              | <Factor>

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

Способ достижения sh, который заключается в использовании рекурсивного спуска для всего остального, но с использованием специализированного алгоритма внутри функции, которая анализирует инфиксные выражения, такого как восхождение по приоритетам (см. это сообщение в блоге Эли Бендерского и / или объяснение в Википедии ).

...