Для начала, определю цели данной серии публикаций.
- Создание функции, выполняющей полноценный парсинг запросов 1С в некоторую древовидную структуру.
- Создание функции, выполняющей обратное преобразование
- Создание обработки "Конструктор запросов" на управляемых формах
Основную актуальность составляет именно третья задача, так как встроенный конструктор запросов работает только в толстом клиенте, а также не является обработкой с открытым кодом - вносить в него изменения невозможно. На инфостарте мелькали публикации с парсерами запросов, но во-первых не рассматривалась методика, а во-вторых я еще не видел парсера, который был бы полностью идентичен встроенному парсеру 1С по функциональности.
- Он должен быть однопроходным (т.к. грамматика языка запросов не предполагает необходимости двупроходной обработки, как, например, грамматика языка C++)
- Он должен включать в себя лексический и синтаксический анализ. В перспективе необходима разработка тонкого анализа связи с метаданными конфигурации (семантический анализ).
- Он должен адекватно обрабатывать исключения
В первой части статьи я опишу разбор математических выражений. Эта тема очень хорошо освещена в русской и зарубежной литературе, впервые я познакомился с ней в книге "О чем не пишут в книгах по Delphi". На хабре достатоно поискать по ключевым словам "Парсер" или "Теория компиляторов". Более того, в данное время существуют генераторы парсеров, которые на основе данных о грамматике языка составляют исходный код парсера (Вики: Сравнение генераторв парсеров (англ.)). Однако, этот метод я рассматривать не буду - настоящий 1С-ник должен полагаться только на свой код.
Итак, какие же знания требуются для написания парсера?
Формальные грамматики.
Для описания грамматики языка Алгол Джоном Бэкусом и Питером Науром была раработана формальная система описания синтаксиса. Она называется БНФ (Бэкуса-Наура форма, BNF Вики: Форма Бэкуса-Наура). Данная система позволяет описывать одни категории с использованием других, постепенно наращивая сложность, и ее вполне реально использовать для решения поставленной задачи. Забегая вперед, скажу, что сама фирма 1С описывает свой язык запросов с помощью этой грамматики. Чтобы в этом убедиться, достаточно открыть справку по языку запросов.
Следующие операторы используются в БНФ:
::= |
присваивание |
| |
Операция ИЛИ |
Имя |
Литерал |
[Имя] |
Необязательный литерал |
(Имя) |
Литерал, повторяющийся 0 или более раз |
При описании грамматики БНФ сначала необходимо дать определение абстракции нижнего уровня:
Цифра :: = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
С помощью этого выражения мы указываем, что литерал может принимать одно из значений '0' ... '9'
Абстракция более выского уровня - вещественное число:
Знак ::= '-' | '+'
Разделитель ::= '.'
Число ::= [Знак] Цифра (Цифра) [ Разделитель (Цифра) ]
Число может иметь знак (+, -) а может не иметь его. Далее должна идти хотя бы одна цифра (или более). Затем может идти разделитель дробной и целой части (а может не идти). Если есть разделитель, то далее может идти одна цифра (или более) (спасибо (9)) .
Простейшее математическое выражение должно удовлетворять следующим требованиям:
- Допустимы операции + - * /
- Приоритет операций: Скобка > Умножение = Деление > Сложение = Вычитание
Основную сложность представляет из себя учет приоритета операций. Для этого любое выражение раскладывается на слагаемые и множители. Далее сначала выполяются операции со скобками, затем с множителями, и в конце со слагаемыми. В терминах БНФ матемтическое выражение описывается так:
Оператор1 ::= '+' | '-'
Оператор2 ::= '*' | '/'
Множитель ::= Число | '(' Выражение ')'
Слагаемое ::= Множитель [Оператор2 Множитель]
Выражение ::= Слагаемое [Оператор1 Слагаемое]
Требование наличия скобок делает нашу грамматкику рекурсивной (на моменте вычисления множителя).
Программная часть.
Теперь определимся с программной частью. Непосредственно синтаксис БНФ будет разбирать синтаксический анализатор. Но с точки зрения грамматики выражение 2+2 является корректным, а 2 + 2 - нет, и для решения этой проблемы (обычно выражения с переносами строки и пробелами читаются легче) будет использоваться лексический анализатор. Его целью будет пропуск незначащих символов и извлечение лексемы (в нашей грамматике это может быть Число, Операция или одна из Скобок, которую он передаст на вход синтаксического анализатора.
Лексический анализатор.
Функция СледующийЛитерал(Литерал, ТекстЗапроса, ТекПоз)
Если ТекПоз <= СтрДлина(ТекстЗапроса) Тогда
// Пропустить пробелы
НезначащиеСимволы = " " + Символы.ПС + Символы.Таб;
Пока Найти(НезначащиеСимволы, Сред(ТекстЗапроса, ТекПоз, 1)) > 0 Цикл
ТекПоз = ТекПоз + 1;
КонецЦикла;
// Извлечь литерал
ТекСимвол = Сред(ТекстЗапроса, ТекПоз, 1);
Если Найти("()*/+-", ТекСимвол) > 0 Тогда
Литерал = ТекСимвол;
ТекПоз = ТекПоз + 1;
ИначеЕсли ЭтоЦифра(ТекСимвол) Тогда
Литерал = ИзвлечьЧисло(ТекстЗапроса, ТекПоз);
Иначе
ВызватьИсключение "Неизвестный символ в позиции " + Формат(ТекПоз, "ЧГ=0");
КонецЕсли;
Возврат Истина;
Иначе
Литерал = Неопределено;
Возврат Ложь;
КонецЕсли;
КонецФункции
Функция ИзвлечьЧисло(ТекстЗапроса, ТекПоз)
ТекСимвол = Сред(ТекстЗапроса, ТекПоз, 1);
Результат = "";
// Целая часть
Пока ЭтоЦифра(ТекСимвол) И ТекПоз <= СтрДлина(ТекстЗапроса) Цикл
ТекПоз = ТекПоз + 1;
Результат = Результат + ТекСимвол;
ТекСимвол = Сред(ТекстЗапроса, ТекПоз, 1);
КонецЦикла;
// Дробная часть
Если ТекСимвол = "." Тогда
Результат = Результат + ".";
ТекПоз = ТекПоз + 1;
ТекСимвол = Сред(ТекстЗапроса, ТекПоз, 1);
Пока ЭтоЦифра(ТекСимвол) И ТекПоз <= СтрДлина(ТекстЗапроса) Цикл
ТекПоз = ТекПоз + 1;
Результат = Результат + ТекСимвол;
ТекСимвол = Сред(ТекстЗапроса, ТекПоз, 1);
КонецЦикла;
КонецЕсли;
Возврат Число(Результат);
КонецФункции
Функция ЭтоЦифра(ТекСимвол)
Возврат ТекСимвол >= "0" И ТекСимвол <= "9";
КонецФункции
Синтаксический анализатор.
Теперь приведу функции синтаксического анализатора. Каждая из них соответствует элементу грамматики БНФ. Отмечу, что в примере этими функциями вычисляются реальные выражения, хотя в реальном парсере они будут всего лишь проверять корректность выражений в запросе. Также в коде отсутствует часть необходимых исключений (эти функции я выдергивал из парсера запросов, который обладает уже гораздо большим функционалом, поэтому частью исключений пришлось пожертвовать - но они вернутся в следующих статьях)
Функция Выражение(ТекЛитерал, ТекстЗапроса, ТекПоз) Экспорт
Если ТекЛитерал = Неопределено Тогда
// При первом вызове необходимо сдвинуть автомат на первую позицию
Если Не СледующийЛитерал(ТекЛитерал, ТекстЗапроса, ТекПоз) Тогда
ВызватьИсключение "Пустая строка";
КонецЕсли;
КонецЕсли;
Результат = Слагаемое(ТекЛитерал, ТекстЗапроса, ТекПоз);
Пока Не ТекЛитерал = Неопределено И Найти("+-", ТекЛитерал) > 0 Цикл
Литерал = ТекЛитерал;
СледующийЛитерал(ТекЛитерал, ТекстЗапроса, ТекПоз);
Если Литерал = "+" Тогда
Результат = Результат + Слагаемое(ТекЛитерал, ТекстЗапроса, ТекПоз);
Иначе
Результат = Результат - Слагаемое(ТекЛитерал, ТекстЗапроса, ТекПоз);
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Функция Слагаемое(ТекЛитерал, ТекстЗапроса, ТекПоз)
Результат = Множитель(ТекЛитерал, ТекстЗапроса, ТекПоз);
Пока Не ТекЛитерал = Неопределено И Найти("*/", ТекЛитерал) > 0 Цикл
Литерал = ТекЛитерал;
СледующийЛитерал(ТекЛитерал, ТекстЗапроса, ТекПоз);
Если Литерал = "*" Тогда
Результат = Результат * Множитель(ТекЛитерал, ТекстЗапроса, ТекПоз);
Иначе
Результат = Результат / Множитель(ТекЛитерал, ТекстЗапроса, ТекПоз);
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Функция Множитель(ТекЛитерал, ТекстЗапроса, ТекПоз)
Если ТекЛитерал = "(" Тогда
Если СледующийЛитерал(ТекЛитерал, ТекстЗапроса, ТекПоз) Тогда
Результат = Выражение(ТекЛитерал, ТекстЗапроса, ТекПоз);
Если ТекЛитерал = ")" Тогда
СледующийЛитерал(ТекЛитерал, ТекстЗапроса, ТекПоз);
Иначе
ВызватьИсключение "Ожидается ) в позиции " + Формат(ТекПоз, "ЧГ=0");
КонецЕсли;
Иначе
ВызватьИсключение "Ожидается выражение в позиции " + Формат(ТекПоз, "ЧГ=0");
КонецЕсли;
ИначеЕсли ЭтоЦифра(Сред(Строка(ТекЛитерал), 1, 1)) Тогда
Результат = ТекЛитерал;
СледующийЛитерал(ТекЛитерал, ТекстЗапроса, ТекПоз);
Иначе
ВызватьИсключение "Неизвестный литерал в позиции " + Формат(ТекПоз, "ЧГ=0");
КонецЕсли;
Возврат Результат;
КонецФункции