Отправьте статью сегодня! Журнал выйдет ..., печатный экземпляр отправим ...
Опубликовать статью

Молодой учёный

Синтаксический анализ выражений методом рекурсивного спуска

Информационные технологии
29.07.2021
1270
Поделиться
Библиографическое описание
Андронычева, Е. М. Синтаксический анализ выражений методом рекурсивного спуска / Е. М. Андронычева. — Текст : непосредственный // Молодой ученый. — 2021. — № 31 (373). — С. 5-10. — URL: https://moluch.ru/archive/373/83435/.


В данной работе разрабатывается программный код для вычисления арифметических выражений методом рекурсивного спуска. Для понимания статьи читатель должен обладать начальными сведениями о C#: уметь компилировать и запускать приложение; знать синтаксис, основные типы данных и структуры управления.

Ключевые слова: рекурсивный спуск, синтаксический анализ, лексический анализ, арифметическое выражение.

Давайте рассмотрим следующую задачу. Пусть у нас есть строка, в которой содержится математическое выражение, например 4 + (20–3) * 2. Необходимо написать алгоритм, который вычислит значение этого выражения. Такая процедура называется синтаксический разбор выражений и является основой всех компиляторов и интерпретаторов языков, электронных таблиц и всех остальных программ, в которых требуется превращать числовые выражения в форму, понятную компьютеру.

Хоть синтаксический разбор может показаться сложным для восприятия, но является достаточно прямолинейным процессом благодаря четко определённой задаче и строгим правилам алгебры. В настоящей статье будет разработан рекурсивный нисходящий синтаксический анализатор, или синтаксический анализатор методом рекурсивного спуска (recursive-descent parser). Для этого будет использован язык программирования C#.

Для начала разберемся, что из себя представляет арифметическое выражение. У арифметических выражений, также как и у других языков, есть определённые правила, по которым они составляются. Эти правила называются грамматикой. Рассмотрим один из вариантов грамматики, который и будет использоваться для написания программы.

// Parser Rules

// expr: plus_minus^ EOF;

// plus_minus: mult_div ((‘+’ | ‘-‘) mult_div)^;

// mult_div: factor ((‘*’ | ‘/’) factor)^;

// factor: number | ‘(‘ expr ‘)’;

Здесь символ ‘^’ означает, что эта часть выражения может повторятся ноль или более раз; factor — подвыражение, которое может содержать число и/или выражение типа expr в скобках; mult_div — подвыражение, которое может содержать одно или несколько подвыражений factor , соединённых знаком умножения или деления; plus_minus — подвыражение, которое может содержать одно или несколько подвыражений mult_div , соединённых знаком сложения или вычитания; expr — выражение, которое содержит конец строки и может содержать подвыражение plus_minus (или не содержать его).

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

Опишем перечисление для типов лексем, которые могут встречаться в выражении. Оно будет содержать элементы, такие как LEFT_BRACKET, RIGHT_BRACKET — открывающаяся и закрывающаяся скобки, OP_PLUS, OP_MINUS, OP_MUL, OP_DIV — операции сложения, вычитания, умножения и деления, NUMBER — число, EOF — конец строки. На Рисунке 1 представлен соответствующий код.

Перечисление типов лексем [создано автором]

Рис. 1. Перечисление типов лексем [создано автором]

Для хранения сведений об лексемах опишем класс, который будет содержать два поля: тип лексемы ( LexemeType ) и саму лексему в переменной строкового типа. Код представлен на Рисунке 2.

Класс Lexeme [создано автором]

Рис. 2. Класс Lexeme [создано автором]

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

1) Если символ является математическим знаком, который поддерживает наша грамматика (т. е. это символы: ‘(’, ‘)’, ‘+’, ‘-’, ‘*’, ‘/’). Без дополнительных операций добавляем его в список с соответствующим типом лексемы.

2) Если символ является цифрой, то это значит, что мы начинаем считывать число. Для этого создаем переменную sb типа StringBuilder , в которую будем добавлять символы. Далее уместно использовать цикл do-while , в котором будем добавлять текущий символ в переменную sb и переходить к следующему символу в строке, пока текущий символ является цифрой, проверяя при этом не является ли он концом строки.

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

Функция лексического анализа [создано автором]

Рис. 3. Функция лексического анализа [создано автором]

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

Класс LexemeBuffer [создано автором]

Рис. 4. Класс LexemeBuffer [создано автором]

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

1) public static string expr(LexemeBuffer lexemes) — метод принимающий в качестве параметра LexemeBuffer — вычисляемую строку и возвращающий вещественное значение — результат выполнения программы, а также сообщение об ошибке типа «Пустая строка»;

2) public static double plusminus(LexemeBuffer lexemes) — метод принимающий в качестве параметра LexemeBuffer — вычисляемую строку и возвращающий вещественное значение — результат выполнения сложения или вычитания;

3) public static double multdiv(LexemeBuffer lexemes) — метод принимающий в качестве параметра LexemeBuffer — вычисляемую строку и возвращающий вещественное значение — результат выполнения умножения или деления;

4) public static double factor(LexemeBuffer lexemes) — метод принимающий в качестве параметра LexemeBuffer — вычисляемую строку и возвращающий вещественное значение — число. А также возвращает такие сообщения об ошибках как «Деление на ноль» и «Отсутствует закрывающаяся скобка».

Класс синтаксического анализа [создано автором]

Рис. 5. Класс синтаксического анализа [создано автором]

Для проверки демонстрации использования анализатора, напишем функцию Main(), представленную на Рисунке 6. В ней будем считывать строку с клавиатуры и выводить форматированный результат на экран.

Функция Main() [создано автором]

Рис. 6. Функция Main() [создано автором]

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

32 + 16 / 2

При вызове функции exp() — входной точки анализатора — из входной строки выбирается лексема. Если она является пустой строкой, то функция выводит ошибку «Пустая строка» и завершает работу. Однако в данном случае лексемой является число 32. Поскольку это не пустая строка, вызывается функция plusminus() . В результате функция plusminus() вызывает multdiv() , а та в свою очередь вызывает factor() . Затем функция factor() может либо рекурсивно вызвать функцию exp() (в случае выражения заключенного в скобки), либо определить значение числа. В нашем случае она возвращает число 32, и управление возвращается функции plusminus() .

Затем происходит выборка следующей лексемы, которой становится оператор + и которая сохраняется в переменную lexeme , и спуск по цепочке начинается снова. Как и раньше, вызывается функция factor() , которая возвращает значение 16. В функции multdiv() считывается следующая лексема — оператор /. Аналогично возвращается последняя лексема 2 и выполняется первая арифметическая операция — деление 16 на 2. Полученный результат возвращается функции plusminus() , где выполняется сложение. В результате вычитания в ответе получается 40.

Литература:

  1. Герберт Шилдт — C# 2.0. Полное руководство, 4-е изд.
  2. Руководство по C# от Microsoft [Электронный ресурс] — https://docs.microsoft.com/
  3. Курс лекции по дисциплине «Технологии программирования» Молчанова С. И. 2021
Можно быстро и просто опубликовать свою научную статью в журнале «Молодой Ученый». Сразу предоставляем препринт и справку о публикации.
Опубликовать статью
Ключевые слова
рекурсивный спуск
синтаксический анализ
лексический анализ
арифметическое выражение
Молодой учёный №31 (373) июль 2021 г.
Скачать часть журнала с этой статьей(стр. 5-10):
Часть 1 (стр. 1-81)
Расположение в файле:
стр. 1стр. 5-10стр. 81

Молодой учёный