Разработка синтаксического анализатора языка запросов на языке 1С
1 Введение |
1.1 Благодарности |
1.2 О чем пойдет речь? |
1.3 Что надо знать, перед тем как приступить к чтению? |
2 Необязательные инструменты использованные в процессе разработки |
3 Введение |
3.1 Примитивный синтаксический анализатор |
3.2 Разработка модели |
3.3 Комбинации парсеров |
3.3.1 Последовательность: e1 e2 |
3.3.2 Упорядоченный выбор: e1 / e2 |
3.3.3 Один или более (e+), нуль или более (e*), необязательно (e?) |
3.3.4 И-предикат (&e), НЕ-предикат (!e) |
3.3.5 Упростим код |
4 Расширение возможностей парсеров |
4.1 Регистро-независимое сравнение символа |
4.2 Сравнение со строкой |
4.3 Регистро-независимое сравнение со строкой |
4.4 Интервал |
4.5 Сравнение с кодом символа |
4.6 Преобразование результата |
|
1 Введение
1.1 Благодарности
Эта публикация никогда не увидела бы свет, если бы не поддержка моей жены — Людмилы. Мой ласковый ангел, эту статью я посвящаю тебе и твоему терпению.
1.2 О чем пойдет речь?
В этой статье будет рассмотрен один из вариантов реализации генератора синтаксических анализаторов. Предложенный вариант один из многих, его не следует рассматривать как единственно возможный, а только как источник идей для разработки собственных анализаторов. Основной целью разработки будет получить парсер для разбора текста запроса, но в учебных целях будет также разработано несколько анализаторов для других простых языков
1.3 Что надо знать, перед тем как приступить к чтению?
Для понимания статьи достаточно знать язык и среду 1С на среднем уровне. Единственную сложность может вызвать использование операторов Выполнить() и Вычислить() .Если использование этих конструкций вызывает у вас сложности, обратитесь к официальной документации
2 Необязательные инструменты использованные в процессе разработки
При разработке кода будут использоваться следующие инструменты:
1. Система юнит-тестирования xUnitFor1C. Так как задача разбора текста достаточно хорошо формализуется, то для упрощения разработки будет использована достаточно развитая система xUnitFor1C. Внимание, эта статья — не инструкция по применению подсистемы юнит-тестирования, поэтому все вопросы касательно функционирования и возможностей использования, пожалуйста, адресуйте разработчикам. Согласно методологии TDD сперва пишется тест, затем код. В этой статье при разработке модели будет применен другой подход – тест является информацией как использовать тот или иной метод. Классический TDD будет продемонстрирован при разработке учебных анализаторов(в следующей публикации).
2. Снегопат. Думаю, не нуждается в рекламе, пользуясь случаем, хочу сказать Александру огромное спасибо за возможность скачивать бесплатные версии.
3. Статья написана с помощью системы генерации текста Scribble (входит в состав DrRacket, скачать можно здесь — http://racket-lang.org/download/, исходный код статьи — здесь)
3 Введение
3.1 Примитивный синтаксический анализатор
Классификация парсеров и типы алгоритмов поверхностно описаны в вики Синтаксический анализ В этой статье буду описывать процесс построения парсера по алгоритму нисходящего разбора, как это описано в статье PEG После того как будут описаны все необходимые примитивы, будет разработана система генерации парсеров на основании простого текстового представления
Парсер, с точки зрения PEG это — функция, принимающая на вход строку и возвращающая либо успех, либо неудача. Самый простой способ задать определения успеха/неудачи следующий —
1. Для успешного разбора — функция возвращает обработанные данные (это может быть как и прочатнный символ/символы, так и произвольные данные), а также остаток строки, получившися после работы парсера. Код выглядит так —
Функция РазобраноУспешно(Значение, Остаток)
СтруктураВозврата = Новый Структура;
СтруктураВозврата.Вставить("Тип",1);
СтруктураВозврата.Вставить("Значение",Значение);
СтруктураВозврата.Вставить("Остаток",Остаток);
Возврат СтруктураВозврата;
Конецфункции
2. В случае если функция не смогла разобрать входную строку, то возвращается структура с типом ошибки и неизмененной строкой
Функция РазобраноНеудачно(Остаток)
СтруктураВозврата = Новый Структура;
СтруктураВозврата.Вставить("Тип",0);
СтруктураВозврата.Вставить("Остаток",Остаток);
Возврат СтруктураВозврата;
Конецфункции
Тогда определение функции разбора одного символа можно описать так
Функция ПарсерОдногоСимвола(Образец,СтрокаАнализа)
СимволАнализа = Сред(СтрокаАнализа,1,1);
Если СимволАнализа = Образец Тогда
Возврат РазобраноУспешно(СимволАнализа,Сред(СтрокаАнализа,2));
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Конецфункции
Для проверки работоспособности кода определим 2 теста
Перем юТест;
Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт
юТест = ЮнитТестирование;
ВсеТесты = Новый Массив;
ВсеТесты.Добавить("Тест_ОдинСимволУспешно");
ВсеТесты.Добавить("Тест_ОдинСимволНеудачно");
Возврат ВсеТесты;
Конецфункции
Процедура Тест_АтомарныйУспешно() Экспорт
образец = Новый структура("Тип,Значение,Остаток",1,"м","ама");
результат = ПарсерОдногоСимвола("м","мама");
юТест.ПроверитьРавенство(результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(результат.Значение, образец.Значение);
юТест.ПроверитьРавенство(результат.Остаток, образец.Остаток);
Конецпроцедуры
Процедура Тест_АтомарныйНеудачно() Экспорт
образец = Новый структура("Тип,Остаток",0,"мама");
результат = ПарсерОдногоСимвола("М","мама");
юТест.ПроверитьРавенство(результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(результат.Остаток, образец.Остаток);
Конецпроцедуры
Если вы используете Снегопат, то запуск тестов можно произвести прямо в конфигураторе (с помощью скрипта xddTestRunner.js), иначе вам необходимо запустить 1С в режиме предприятия, и с помощью обработки xddTestRunner.epf проверить выполнение тестов. Определенные нами 2 теста отработают вполне корректно и результат тестирования будет положительным.
3.2 Разработка модели
Как следует из документации, последовательность парсеров — это комбинация 2 парсеров, работающая следующим образом —
- 1. Получить результат работы первого парсера
- 2. Если результат работы — неудача, то парсер завершает работу и возвращает неудача
- 3. Проверяется результат работы второго парсера
- 4. Если результат работы — неудача, то парсер завершает работу и возвращает неудача
- 5. Оба парсера завершены успешно, возвращается истина
Попытаемся реализовать это описание в виде функции
Функция ПоследовательностьПарсеров(Первый,Второй,СтрокаАнализа)
РезультатРаботыПарсера = вызвать первый парсер;
Если Неудача(Первый) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
РезультатРаботыПарсера = вызвать второй парсер;
Если Неудача(Первый) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Возврат РазобраноУспешно(Что вернуть?,Как получить остаток строки?);
Конецфункции
Простая на вид функция поставила перед нами столько вопросов. Будем решать их последовательно. И первое к чему приступим — проектирование структуры нашего парсера.
Для начала — определимся с режимами, в которых будет существовать наш парсер. В проектируемой модели будет 2 режима — режим компиляции и режим выполнения . Во время компиляции мы будем строить парсер, во время выполнения — разбирать полученную строку с помощью парсера
В нашей реализации парсер будет представляться структурой с обязательным полем «Тип» и списком аргументов которые будут применяться для каждого типа парсера Возможная реализация функции ПостроитьПарсер приведена ниже
Функция ПостроитьПарсер(Тип,Аргумент1 = Неопределено, Аргумент2 = Неопределено)
Если Тип = "match" Тогда
Возврат Новый Структура("Тип,Образец",Тип,Аргумент1);
Иначе
Вызватьисключение "Неизвестный тип построения парсера """+Тип+"""";
Конецесли;
Конецфункции
А функция применения парсера может тогда выглядит так
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
Если Парсер.Тип = "match" Тогда
Возврат ПарсерОдногоСимвола(Парсер.Образец,СтрокаАнализа);
Иначе
Вызватьисключение "Неизвестный тип для приеменения парсера """+Парсер.Тип+"""";
Конецесли;
Конецфункции
Тест для проверки и документирования варианта вызова —
Процедура Тест_АтомарныйУдачноНоваяМодель() Экспорт
Парсер = ПостроитьПарсер("match","м");
Результат = ПрименитьПарсер(Парсер,"мама");
Образец = РазобраноУспешно("м","ама");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Результат.Остаток, образец.Остаток);
юТест.ПроверитьРавенство(Результат.Значение, образец.Значение);
Конецпроцедуры
Теперь наша функция для построения последовательности, выглядит так
Функция ПоследовательностьПарсеров(Первый,Второй,СтрокаАнализа)
РезультатРаботыПарсера1 = ПрименитьПарсер(Первый,СтрокаАнализа);
Если Неудача(РезультатРаботыПарсера1) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
РезультатРаботыПарсера2 = ПрименитьПарсер(Второй,Как получить остаток строки после первого парсера?);
Если Неудача(РезультатРаботыПарсера2) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Возврат РазобраноУспешно(Что вернуть?,Как получить остаток строки?);
Конецфункции
В коде используется неопределенная функция Неудача() . Она, как и прочие сервисные функции, для обработки результата парсера описаны ниже.
Функция кнстНеудача()
Возврат 0;
Конецфункции
Функция кнстУспех()
Возврат 1;
Конецфункции
Функция РазобраноУспешно(Значение,Остаток)
СтруктураВозврата = Новый Структура;
СтруктураВозврата.Вставить("Тип",кнстУспех());
СтруктураВозврата.Вставить("Значение",Значение);
СтруктураВозврата.Вставить("Остаток",Остаток);
Возврат СтруктураВозврата;
Конецфункции
Функция РазобраноНеудачно(Остаток)
СтруктураВозврата = Новый Структура;
СтруктураВозврата.Вставить("Тип",кнстНеудача());
СтруктураВозврата.Вставить("Остаток",Остаток);
Возврат СтруктураВозврата;
Конецфункции
Функция Успех(Значение)
Возврат
ТипЗнч(Значение) = Тип("Структура")
И Значение.Свойство("Тип")
И Значение.Тип = кнстУспех();
Конецфункции
Функция Неудача(Значение)
Возврат
ТипЗнч(Значение) = Тип("Структура")
И Значение.Свойство("Тип")
И Значение.Тип = кнстНеудача();
Конецфункции
Функция Остаток(Значение)
Если Не (Успех(Значение)
Или Неудача(Значение)) Тогда
Вызватьисключение "Неверное определение структуры парсера"
Конецесли;
Возврат Значение.Остаток;
Конецфункции
Функция Значение(Значение)
Если Не (Успех(Значение)
И Значение.Свойство("Значение")) Тогда
Вызватьисключение "Неверное определение структуры парсера"
Конецесли;
Возврат Значение.Значение;
Конецфункции
С учетом введенных функций, наша функция комбинации последовательности принимает вид
Функция ПоследовательностьПарсеров(Первый,Второй,СтрокаАнализа)
РезультатРаботыПарсера1 = ПрименитьПарсер(Первый,СтрокаАнализа);
Если Неудача(РезультатРаботыПарсера1) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
РезультатРаботыПарсера2 = ПрименитьПарсер(Второй,Остаток(РезультатРаботыПарсера1));
Если Неудача(РезультатРаботыПарсера2) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Возврат РазобраноУспешно(Что вернуть?,Остаток(РезультатРаботыПарсера2));
Конецфункции
Осталось определиться, что будет возвращать эта функция. Нам необходимо вернуть оба результата работы парсеров, и самым простым способом будет вернуть массив. В итоге функция принимает вид
Функция ПоследовательностьПарсеров(Первый,Второй,СтрокаАнализа)
РезультатРаботыПарсера1 = ПрименитьПарсер(Первый,СтрокаАнализа);
Если Неудача(РезультатРаботыПарсера1) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
РезультатРаботыПарсера2 = ПрименитьПарсер(Второй,Остаток(РезультатРаботыПарсера1));
Если Неудача(РезультатРаботыПарсера2) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
МассивВозврата = Новый Массив;
МассивВозврата.Добавить(Значение(РезультатРаботыПарсера1));
МассивВозврата.Добавить(Значение(РезультатРаботыПарсера2));
Возврат РазобраноУспешно(МассивВозврата,Остаток(РезультатРаботыПарсера2));
Конецфункции
Напишем простой тест для проверки тестовой грамматики
|
‹ТестоваяГрамматика› |
::= |
‹м› ‹а› |
|
‹м› |
::= |
м |
|
‹а› |
::= |
а |
Процедура Тест_Последовательно() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
Результат = ПоследовательностьПарсеров(БукваМ,БукваА,"мама");
МассивРезультата = Новый Массив;
МассивРезультата.Добавить("м");
МассивРезультата.Добавить("а");
Образец = РазобраноУспешно(МассивРезультата,"ма");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[1], Значение(образец)[1]);
Конецпроцедуры
Если вы что-то недопоняли -пройдитесь по коду с отладчиком, потому как этот подход будет дальше использован, будет развиваться и усложняться. Другими словами — код приведенный выше — самый сложный момент на всю публикацию. Его надо понять, что бы дальнейшие рассуждения не стали «китайской грамотой».
3.3 Комбинации парсеров
3.3.1 Последовательность: e1 e2
В предыдущем параграфе мы разработали функцию парсера последовательности из 2 парсеров. Но 2 парсера в последовательности — не самый частый случай в практике, чаще последовательность содержит куда больше элементов.
Изменим представление для хранения списка парсеров в последовательности. Для этого используем массив
Функция ПоследовательностьПарсеров(ПоследовательностьПарсеров,СтрокаАнализа)
МассивВозврата = Новый Массив;
СтрокаРаботы = СтрокаАнализа;
Для Каждого Парсер Из ПоследовательностьПарсеров Цикл
РезультатРаботы = ПрименитьПарсер(Парсер,СтрокаРаботы);
Если Неудача(РезультатРаботы) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
МассивВозврата.Добавить(Значение(РезультатРаботы));
СтрокаРаботы = Остаток(РезультатРаботы);
Конеццикла;
Возврат РазобраноУспешно(МассивВозврата,СтрокаРаботы);
Конецфункции
Функция ПостроитьПарсер(Тип,Аргумент1 = Неопределено, Аргумент2 = Неопределено)
Если Тип = "match" Тогда
Возврат Новый Структура("Тип,Образец",Тип,Аргумент1);
Иначеесли Тип = "seq" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначе
Вызватьисключение "Неизвестный тип построения парсера """+Тип+"""";
Конецесли;
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
Если Парсер.Тип = "match" Тогда
Возврат ПарсерОдногоСимвола(Парсер.Образец,СтрокаАнализа);
Иначеесли Парсер.Тип = "seq" Тогда
Возврат ПоследовательностьПарсеров(Парсер.Последовательность,СтрокаАнализа);
Иначе
Вызватьисключение "Неизвестный тип для приеменения парсера """+Парсер.Тип+"""";
Конецесли;
Конецфункции
Проверим работоспособность парсера последовательности на следующей грамматике
|
‹тест1› |
::= |
‹м› ‹а› ‹м› ‹а› |
|
‹м› |
::= |
м |
|
‹а› |
::= |
а |
Процедура Тест_Последовательно() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
МассивПарсеров = Новый Массив;
МассивПарсеров.Добавить(БукваМ);
МассивПарсеров.Добавить(БукваА);
МассивПарсеров.Добавить(БукваМ);
МассивПарсеров.Добавить(БукваА);
ТестовыйПарсер = ПостроитьПарсер("seq",МассивПарсеров);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = Новый Массив;
МассивРезультата.Добавить("м");
МассивРезультата.Добавить("а");
МассивРезультата.Добавить("м");
МассивРезультата.Добавить("а");
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[1], Значение(образец)[1]);
юТест.ПроверитьРавенство(Значение(Результат)[2], Значение(образец)[2]);
юТест.ПроверитьРавенство(Значение(Результат)[3], Значение(образец)[3]);
Конецпроцедуры
Как и ожидалось — тест отрабатывает успешно, но чисто с эстетической точки зрения последовательность кода
МассивПарсеров = Новый Массив;
МассивПарсеров.Добавить(БукваМ);
МассивПарсеров.Добавить(БукваА);
МассивПарсеров.Добавить(БукваМ);
МассивПарсеров.Добавить(БукваА);
выглядит некрасиво. Давайте преобразуем эти строки в вызов функции ВМассив(БукваМ,БукваА,БукваМ,БукваА)
Функция ДобавитьВМассив(МассивДобавления,Элемент)
Если Элемент = Неопределено Тогда
Возврат МассивДобавления;
Конецесли;
МассивДобавления.Добавить(Элемент);
Конецфункции
Функция ВМассив(Элемент1=Неопределено,Элемент2=Неопределено,Элемент3=Неопределено,Элемент4=Неопределено,Элемент5=Неопределено,Элемент6=Неопределено)
// Согласен, что выглядит некрасиво, но 1С - не lisp, приходится терпеть вот такую ерунду.
МассивВозврата = Новый Массив;
Для х = 1 По 6 Цикл
ДобавитьВМассив(МассивВозврата,Вычислить("Элемент"+х))
Конеццикла;
Возврат МассивВозврата;
Конецфункции
Если однажды нам не хватить аргументов в функции ВМассив, мы всегда можем расширить список, добавив необходимое число аргументов в объявление функции. После такого изменения тест можно переписать, например, так
Процедура Тест_Последовательно() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
МассивПарсеров = Новый Массив;
МассивПарсеров.Добавить(БукваМ);
МассивПарсеров.Добавить(БукваА);
МассивПарсеров.Добавить(БукваМ);
МассивПарсеров.Добавить(БукваА);
ТестовыйПарсер = ПостроитьПарсер("seq",МассивПарсеров);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = Новый Массив;
МассивРезультата.Добавить("м");
МассивРезультата.Добавить("а");
МассивРезультата.Добавить("м");
МассивРезультата.Добавить("а");
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[1], Значение(образец)[1]);
юТест.ПроверитьРавенство(Значение(Результат)[2], Значение(образец)[2]);
юТест.ПроверитьРавенство(Значение(Результат)[3], Значение(образец)[3]);
Конецпроцедуры
3.3.2 Упорядоченный выбор: e1 / e2
Рассмотрим определение для операции выбора — Оператор выбора e1 / e2 сначала вызывает e1 и, если e1 успешно, возвращает её результат. Иначе, если e1проваливается, оператор выбора восстанавливает входную строку в состояние, предшествующее вызову e1, и вызывает e2, возвращая её результат.
Код парсера для 2 аргументов выглядит так
Функция ОперацияВыбор(Первый,Второй,СтрокаАнализа)
РезультатРаботыПарсера1 = ПрименитьПарсер(Первый,СтрокаАнализа);
Если Успех(РезультатРаботыПарсера1) Тогда
Возврат РезультатРаботыПарсера1;
Конецесли;
Возврат ПрименитьПарсер(Второй,СтрокаАнализа);
Конецфункции
Расширим определение оператора выбора таким образом — Оператор выбора последовательно применяет входящие в его состав альтернативы до тех пор, пока не встретиться парсер вернувший Успех() или не будут просмотрены все альтернативы. Возвращает значение первого успешного парсера и завершает свою работу или значение вычисления последней альтернативы
В нашем коде реализовано это будет так
Функция ОперацияВыбор(ПоследовательностьПарсеров,СтрокаАнализа)
Для Каждого Парсер Из ПоследовательностьПарсеров Цикл
РезультатРаботы = ПрименитьПарсер(Парсер,СтрокаАнализа);
Если Успех(РезультатРаботы) Тогда
Возврат РезультатРаботы
Конецесли;
Конеццикла;
Возврат РезультатРаботы;
Конецфункции
Добавим определение оператора выбора в функции ПостроитьПарсер() иПрименитьПарсер
Функция ПостроитьПарсер(Тип,Аргумент1 = Неопределено, Аргумент2 = Неопределено)
Если Тип = "match" Тогда
Возврат Новый Структура("Тип,Образец",Тип,Аргумент1);
Иначеесли Тип = "seq" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "/" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначе
Вызватьисключение "Неизвестный тип построения парсера """+Тип+"""";
Конецесли;
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
Если Парсер.Тип = "match" Тогда
Возврат ПарсерОдногоСимвола(Парсер.Образец,СтрокаАнализа);
Иначеесли Парсер.Тип = "seq" Тогда
Возврат ПоследовательностьПарсеров(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "/" Тогда
Возврат ОперацияВыбор(Парсер.Последовательность,СтрокаАнализа);
Иначе
Вызватьисключение "Неизвестный тип для приеменения парсера """+Парсер.Тип+"""";
Конецесли;
Конецфункции
Традиционно закончим работу с новым оператором простым тестом для грамматики
|
‹тест1› |
::= |
‹М/П› ‹А› ‹М/П› ‹А› |
|
‹М/П› |
::= |
м | п |
|
‹А› |
::= |
а |
Процедура Тест_ОперацияВыбор() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваП = ПостроитьПарсер("match","п");
БукваА = ПостроитьПарсер("match","а");
БукваМилиП = ПостроитьПарсер("/",ВМассив(БукваМ,БукваП));
ТестовыйПарсер = ПостроитьПарсер("seq",ВМассив(БукваМилиП,БукваА,БукваМилиП,БукваА));
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
МассивРезультата = ВМассив("п","а","п","а");
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[1], Значение(образец)[1]);
юТест.ПроверитьРавенство(Значение(Результат)[2], Значение(образец)[2]);
юТест.ПроверитьРавенство(Значение(Результат)[3], Значение(образец)[3]);
Конецпроцедуры
Запустим его, удостоверимся что все работает и перейдем к следующему этапу.
3.3.3 Один или более (e+), нуль или более (e*), необязательно (e?)
Как и ранее, возьмем определения для операторов с wiki — Операторы нуль-или-более, один-или-более и необязательности поглощают соответственно нуль или более, одно или более, или нуль либо одно последовательное появление своего подвыражения e. В отличие от КС-грамматик и регулярных выражений, эти операторы всегда являются жадными, и поглощают столько входных экземпляров, сколько могут.
И переведем каждый из этик операторов в код (как видите в написании парсеров нет ничего сложного — читай постановку задачи и пиши код)
Парсер «Один или более» можно определить так
Функция ОдинИлиБолее(Парсер,СтрокаАнализа)
СтрокаРаботы = СтрокаАнализа;
МассивРезультата = Новый Массив;
Пока Истина Цикл
Результат = ПрименитьПарсер(Парсер,СтрокаРаботы);
Если Неудача(Результат) Тогда
Прервать;
Конецесли;
МассивРезультата.Добавить(Значение(Результат));
СтрокаРаботы = Остаток(Результат);
Конеццикла;
Если МассивРезультата.Количество() = 0 Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Иначе
Возврат РазобраноУспешно(МассивРезультата,СтрокаРаботы);
Конецесли;
Конецфункции
Изменим функции ПостроитьПарсер и ПрименитьПарсер
Функция ПостроитьПарсер(Тип,Аргумент1 = Неопределено, Аргумент2 = Неопределено)
Если Тип = "match" Тогда
Возврат Новый Структура("Тип,Образец",Тип,Аргумент1);
Иначеесли Тип = "seq" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "/" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "+" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначе
Вызватьисключение "Неизвестный тип построения парсера """+Тип+"""";
Конецесли;
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
Если Парсер.Тип = "match" Тогда
Возврат ПарсерОдногоСимвола(Парсер.Образец,СтрокаАнализа);
Иначеесли Парсер.Тип = "seq" Тогда
Возврат ПоследовательностьПарсеров(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "/" Тогда
Возврат ОперацияВыбор(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "+" Тогда
Возврат ОдинИлиБолее(Парсер.Парсер,СтрокаАнализа);
Иначе
Вызватьисключение "Неизвестный тип для приеменения парсера """+Парсер.Тип+"""";
Конецесли;
Конецфункции
Напишем тест. Кстати, обратите внимание, оператор «+» возвращает массив с результатами работы вложенного парсера.
Процедура Тест_ОдинИлиБолее_Один() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("+",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"ма");
МассивРезультата = ВМассив(ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
Конецпроцедуры
Процедура Тест_ОдинИлиБолее_Более() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваП = ПостроитьПарсер("match","п");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("+",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"),ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
юТест.ПроверитьРавенство(Значение(Результат)[1][0], Значение(образец)[1][0]);
юТест.ПроверитьРавенство(Значение(Результат)[1][1], Значение(образец)[1][1]);
Конецпроцедуры
Процедура Тест_ОдинИлиБолее_Ноль() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("+",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
Образец = РазобраноНеудачно("папа");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
Конецпроцедуры
Парсер «Ноль или более» определяется по аналогии с парсером «Один или более», но всегда возвращает успешный результат разбора
Функция НольИлиБолее(Парсер,СтрокаАнализа)
СтрокаРаботы = СтрокаАнализа;
МассивРезультата = Новый Массив;
Пока Истина Цикл
Результат = ПрименитьПарсер(Парсер,СтрокаРаботы);
Если Неудача(Результат) Тогда
Прервать;
Конецесли;
МассивРезультата.Добавить(Значение(Результат));
СтрокаРаботы = Остаток(Результат);
Конеццикла;
Возврат РазобраноУспешно(МассивРезультата,СтрокаРаботы);
Конецфункции
Изменим функции ПостроитьПарсер и ПрименитьПарсер, что бы добавить поддержку парсера «Ноль и более»
Функция ПостроитьПарсер(Тип,Аргумент1 = Неопределено, Аргумент2 = Неопределено)
Если Тип = "match" Тогда
Возврат Новый Структура("Тип,Образец",Тип,Аргумент1);
Иначеесли Тип = "seq" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "/" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "+" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначеесли Тип = "*" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначе
Вызватьисключение "Неизвестный тип построения парсера """+Тип+"""";
Конецесли;
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
Если Парсер.Тип = "match" Тогда
Возврат ПарсерОдногоСимвола(Парсер.Образец,СтрокаАнализа);
Иначеесли Парсер.Тип = "seq" Тогда
Возврат ПоследовательностьПарсеров(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "/" Тогда
Возврат ОперацияВыбор(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "+" Тогда
Возврат ОдинИлиБолее(Парсер.Парсер,СтрокаАнализа);
Иначеесли Парсер.Тип = "*" Тогда
Возврат НольИлиБолее(Парсер.Парсер,СтрокаАнализа);
Иначе
Вызватьисключение "Неизвестный тип для приеменения парсера """+Парсер.Тип+"""";
Конецесли;
Конецфункции
Традиционные тесты для проверки
Процедура Тест_НольИлиБолее_Ноль() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("*",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
МассивРезультата = Новый Массив;
Образец = РазобраноУспешно(МассивРезультата,"папа");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат).Количество(), Значение(образец).Количество());
Конецпроцедуры
Процедура Тест_НольИлиБолее_Более() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("*",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"),ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
юТест.ПроверитьРавенство(Значение(Результат)[1][0], Значение(образец)[1][0]);
юТест.ПроверитьРавенство(Значение(Результат)[1][1], Значение(образец)[1][1]);
Конецпроцедуры
Остался последний простой оператор — Необязательно. Определим его таким образом
Функция Необязательно(Парсер,СтрокаАнализа)
Результат = ПрименитьПарсер(Парсер,СтрокаАнализа);
Если Успех(Результат) Тогда
Возврат Результат;
Иначе
Возврат РазобраноУспешно(Неопределено,СтрокаАнализа);
Конецесли;
Конецфункции
Функция ПостроитьПарсер(Тип,Аргумент1 = Неопределено, Аргумент2 = Неопределено)
Если Тип = "match" Тогда
Возврат Новый Структура("Тип,Образец",Тип,Аргумент1);
Иначеесли Тип = "seq" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "/" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "+" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначеесли Тип = "*" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначеесли Тип = "?" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначе
Вызватьисключение "Неизвестный тип построения парсера """+Тип+"""";
Конецесли;
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
Если Парсер.Тип = "match" Тогда
Возврат ПарсерОдногоСимвола(Парсер.Образец,СтрокаАнализа);
Иначеесли Парсер.Тип = "seq" Тогда
Возврат ПоследовательностьПарсеров(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "/" Тогда
Возврат ОперацияВыбор(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "+" Тогда
Возврат ОдинИлиБолее(Парсер.Парсер,СтрокаАнализа);
Иначеесли Парсер.Тип = "*" Тогда
Возврат НольИлиБолее(Парсер.Парсер,СтрокаАнализа);
Иначеесли Парсер.Тип = "?" Тогда
Возврат Необязательно(Парсер.Парсер,СтрокаАнализа);
Иначе
Вызватьисключение "Неизвестный тип для приеменения парсера """+Парсер.Тип+"""";
Конецесли;
Конецфункции
Конечно же, тесты. Без них – никак!
Процедура Тест_Необязательно_Есть() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("?",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив("м","а");
Образец = РазобраноУспешно(МассивРезультата,"ма");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
Конецпроцедуры
Процедура Тест_Необязательно_Нет() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("?",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
Образец = РазобраноУспешно(Неопределено,"папа");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат), Значение(образец));
Конецпроцедуры
3.3.4 И-предикат (&e), НЕ-предикат (!e)
Что бы не изобретать велосипед и не страдать приступами альтернативного мышления, определения для операторов «&» и «!» возьмем с wiki — Выражение &e вызывает подвыражение e, и возвращает успех, если e успешно, и провал в противном случае, но никогда не поглощает ввода. Аналогично, выражение !e срабатывает успешно, если e проваливается и проваливается, если e успешно, так же не поглощая ввода.
Сложностей в определении этих операторов нет.
Функция ИПредикат(Парсер,СтрокаАнализа)
Результат = ПрименитьПарсер(Парсер,СтрокаАнализа);
Если Успех(Результат) Тогда
Возврат РазобраноУспешно(Неопределено,СтрокаАнализа);
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Конецфункции
Обратите внимание — в функцию РазобраноУспешно(…) передается входная строка, а не остаток от применения парсера.
Аналогично определяем и Не-предикат
Функция НеПредикат(Парсер,СтрокаАнализа)
Результат = ПрименитьПарсер(Парсер,СтрокаАнализа);
Если Неудача(Результат) Тогда
Возврат РазобраноУспешно(Неопределено,СтрокаАнализа);
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Конецфункции
Добавим операторы «!» и «&» в функции ПостроитьПарсер() иПрименитьПарсер()
Функция ПостроитьПарсер(Тип,Аргумент1 = Неопределено, Аргумент2 = Неопределено)
Если Тип = "match" Тогда
Возврат Новый Структура("Тип,Образец",Тип,Аргумент1);
Иначеесли Тип = "seq" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "/" Тогда
Возврат Новый Структура("Тип,Последовательность",Тип,Аргумент1);
Иначеесли Тип = "+" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначеесли Тип = "*" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначеесли Тип = "?" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначеесли Тип = "!" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначеесли Тип = "&" Тогда
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент1);
Иначе
Вызватьисключение "Неизвестный тип построения парсера """+Тип+"""";
Конецесли;
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
Если Парсер.Тип = "match" Тогда
Возврат ПарсерОдногоСимвола(Парсер.Образец,СтрокаАнализа);
Иначеесли Парсер.Тип = "seq" Тогда
Возврат ПоследовательностьПарсеров(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "/" Тогда
Возврат ОперацияВыбор(Парсер.Последовательность,СтрокаАнализа);
Иначеесли Парсер.Тип = "+" Тогда
Возврат ОдинИлиБолее(Парсер.Парсер,СтрокаАнализа);
Иначеесли Парсер.Тип = "*" Тогда
Возврат НольИлиБолее(Парсер.Парсер,СтрокаАнализа);
Иначеесли Парсер.Тип = "?" Тогда
Возврат Необязательно(Парсер.Парсер,СтрокаАнализа);
Иначеесли Парсер.Тип = "!" Тогда
Возврат НеПредикат(Парсер.Парсер,СтрокаАнализа);
Иначеесли Парсер.Тип = "&" Тогда
Возврат ИПредикат(Парсер.Парсер,СтрокаАнализа);
Иначе
Вызватьисключение "Неизвестный тип для приеменения парсера """+Парсер.Тип+"""";
Конецесли;
Конецфункции
И сразу тесты для проверки работы.
Процедура Тест_ИПредикат() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("seq",ВМассив(БукваМиА,ПостроитьПарсер("&",БукваМ)));
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"ма");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
Конецпроцедуры
Процедура Тест_НеПредикат() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваП = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("seq",ВМассив(БукваМиА,ПостроитьПарсер("!",БукваП)));
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"ма");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
Конецпроцедуры
3.3.5 Упростим код
Давайте посмотрим на определение функции ПостроитьПарсер(… иПрименитьПарсер(…) и подумаем — как она будет расти при добавлении новых операторов? Правильный ответ — быстро. Добавление нового оператора будет приводить к добавлению как минимум 2 строк кода и уже при 15 операторах наша функция будет не влезать в экран, работать с ней станет сложно
Откажемся от второго аргумента в функции ПостроитьПарсер(…) и применим паттерн «Pluggable Selector» к функции ПрименитьПарсер(…) . Наш код преобразуется в такой
Перем СписокФункций;
Функция ЗарегистрироватьФункцииПарсера(ТипПарсера,ФункцияРазбора)
СписокФункций[ТипПарсера] = ФункцияРазбора
Конецфункции
Функция ПостроитьПарсер(Тип,Аргумент = Неопределено)
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент);
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
ФункцияПарсера = СписокФункций [Парсер.Тип];
Если ФункцияПарсера = Неопределено Тогда
Вызватьисключение "Неизвестный тип парсера "+Парсер.Тип;
Конецесли;
Возврат Вычислить(ФункцияПарсера + "(Парсер,СтрокаАнализа)");
Конецфункции
Процедура Инициализация()
СписокФункций = Новый Соответствие;
ЗарегистрироватьФункцииПарсера ("match", "ПарсерОдногоСимвола");
ЗарегистрироватьФункцииПарсера ("seq", "ПоследовательностьПарсеров");
ЗарегистрироватьФункцииПарсера ("/", "ОперацияВыбор");
ЗарегистрироватьФункцииПарсера ("+", "ОдинИлиБолее");
ЗарегистрироватьФункцииПарсера ("*", "НольИлиБолее");
ЗарегистрироватьФункцииПарсера ("?", "Необязательно");
ЗарегистрироватьФункцииПарсера ("!", "НеПредикат");
ЗарегистрироватьФункцииПарсера ("&", "ИПредикат");
Конецпроцедуры
Учитывая что мы поменяли интерфейс вызова функций (раньше передавались аргументы, а теперь они завернуты в структуру и храняться по ключу «Парсер», тесты написаные для старой версии интерпретатора парсеров все закончатся неудачно. Но менять их не будем. Лучше изменим код под тесты. Новый вариант интерпретатора выглядит так
Перем юТест;
Перем СписокФункций;
Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт
юТест = ЮнитТестирование;
ВсеТесты = Новый Массив;
ВсеТесты.Добавить("Тест_Последовательно");
ВсеТесты.Добавить("Тест_ОперацияВыбор");
ВсеТесты.Добавить("Тест_ОдинИлиБолее_Один");
ВсеТесты.Добавить("Тест_ОдинИлиБолее_Более");
ВсеТесты.Добавить("Тест_ОдинИлиБолее_Ноль");
ВсеТесты.Добавить("Тест_НольИлиБолее_Ноль");
ВсеТесты.Добавить("Тест_НольИлиБолее_Более");
ВсеТесты.Добавить("Тест_Необязательно_Есть");
ВсеТесты.Добавить("Тест_Необязательно_Нет");
ВсеТесты.Добавить("Тест_ИПредикат");
ВсеТесты.Добавить("Тест_НеПредикат");
Возврат ВсеТесты;
КонецФункции
Функция ДобавитьВМассив(МассивДобавления,Элемент)
Если Элемент = Неопределено Тогда
Возврат МассивДобавления;
Конецесли;
МассивДобавления.Добавить(Элемент);
Конецфункции
Функция ВМассив(Элемент1=Неопределено,Элемент2=Неопределено,Элемент3=Неопределено,Элемент4=Неопределено,Элемент5=Неопределено,Элемент6=Неопределено)
//Выглядит некрасиво, согласен, но 1С - не lisp, приходится терпеть вот такую ерунду.
МассивВозврата = Новый Массив;
Для х = 1 По 6 Цикл
ДобавитьВМассив(МассивВозврата,Вычислить("Элемент"+х))
Конеццикла;
Возврат МассивВозврата;
Конецфункции
Функция кнстНеудача()
Возврат 0;
Конецфункции
Функция кнстУспех()
Возврат 1;
Конецфункции
Функция РазобраноУспешно(Значение,Остаток)
Возврат Новый Структура("Тип,Значение,Остаток",кнстУспех(),Значение,Остаток);
Конецфункции
Функция РазобраноНеудачно(Остаток)
Возврат Новый Структура("Тип,Остаток",кнстНеудача(),Остаток);
Конецфункции
Функция Успех(Значение)
Возврат ТипЗнч(Значение) = Тип("Структура") И Значение.Свойство("Тип") И Значение.Тип = кнстУспех();
Конецфункции
Функция Неудача(Значение)
Возврат ТипЗнч(Значение) = Тип("Структура") И Значение.Свойство("Тип") И Значение.Тип = кнстНеудача();
Конецфункции
Функция Остаток(Значение)
Если Не (Успех(Значение) Или Неудача(Значение)) Тогда
Вызватьисключение "Неверное определение структуры парсера"
Конецесли;
Возврат Значение.Остаток;
Конецфункции
Функция Значение(Значение)
Если Не (Успех(Значение) И Значение.Свойство("Значение")) Тогда
Вызватьисключение "Неверное определение структуры парсера"
Конецесли;
Возврат Значение.Значение;
Конецфункции
Функция ПарсерОдногоСимвола(Парсер,СтрокаАнализа)
СимволАнализа = Сред(СтрокаАнализа,1,1);
Если СимволАнализа = Парсер.Парсер Тогда
Возврат РазобраноУспешно(СимволАнализа,Сред(СтрокаАнализа,2));
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Конецфункции
Функция ПоследовательностьПарсеров(ПоследовательностьПарсеров,СтрокаАнализа)
МассивВозврата = Новый Массив;
СтрокаРаботы = СтрокаАнализа;
Для Каждого Парсер Из ПоследовательностьПарсеров.Парсер Цикл
РезультатРаботы = ПрименитьПарсер(Парсер,СтрокаРаботы);
Если Неудача(РезультатРаботы) Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
МассивВозврата.Добавить(Значение(РезультатРаботы));
СтрокаРаботы = Остаток(РезультатРаботы);
Конеццикла;
Возврат РазобраноУспешно(МассивВозврата,СтрокаРаботы);
Конецфункции
Функция ОперацияВыбор(ПоследовательностьПарсеров,СтрокаАнализа)
Для Каждого Парсер Из ПоследовательностьПарсеров.Парсер Цикл
РезультатРаботы = ПрименитьПарсер(Парсер,СтрокаАнализа);
Если Успех(РезультатРаботы) Тогда
Возврат РезультатРаботы
Конецесли;
Конеццикла;
Возврат РезультатРаботы;
Конецфункции
Функция ОдинИлиБолее(Парсер,СтрокаАнализа)
СтрокаРаботы = СтрокаАнализа;
МассивРезультата = Новый Массив;
РабочийПарсер = Парсер.Парсер;
Пока Истина Цикл
Результат = ПрименитьПарсер(РабочийПарсер,СтрокаРаботы);
Если Неудача(Результат) тогда
Прервать;
КонецЕсли;
МассивРезультата.Добавить(Значение(Результат));
СтрокаРаботы = Остаток(Результат);
КонецЦикла;
Если МассивРезультата.Количество() = 0 Тогда
Возврат РазобраноНеудачно(СтрокаАнализа);
Иначе
Возврат РазобраноУспешно(МассивРезультата,СтрокаРаботы);
КонецЕсли;
КонецФункции
Функция НольИлиБолее(Парсер,СтрокаАнализа)
СтрокаРаботы = СтрокаАнализа;
МассивРезультата = Новый Массив;
РабочийПарсер = Парсер.Парсер;
Пока Истина Цикл
Результат = ПрименитьПарсер(РабочийПарсер,СтрокаРаботы);
Если Неудача(Результат) тогда
Прервать;
КонецЕсли;
МассивРезультата.Добавить(Значение(Результат));
СтрокаРаботы = Остаток(Результат);
КонецЦикла;
Возврат РазобраноУспешно(МассивРезультата,СтрокаРаботы);
КонецФункции
Функция Необязательно(Парсер,СтрокаАнализа)
Результат = ПрименитьПарсер(Парсер.Парсер,СтрокаАнализа);
Если Успех(Результат) Тогда
Возврат Результат;
Иначе
Возврат РазобраноУспешно(Неопределено,СтрокаАнализа);
Конецесли;
Конецфункции
Функция ИПредикат(Парсер,СтрокаАнализа)
Результат = ПрименитьПарсер(Парсер.Парсер,СтрокаАнализа);
Если Успех(Результат) Тогда
Возврат РазобраноУспешно(Неопределено,СтрокаАнализа);
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Конецфункции
Функция НеПредикат(Парсер,СтрокаАнализа)
Результат = ПрименитьПарсер(Парсер.Парсер,СтрокаАнализа);
Если Неудача(Результат) Тогда
Возврат РазобраноУспешно(Неопределено,СтрокаАнализа);
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Конецфункции
Функция ЗарегистрироватьФункцииПарсера(ТипПарсера,ФункцияРазбора)
СписокФункций[ТипПарсера] = ФункцияРазбора
Конецфункции
Функция ПостроитьПарсер(Тип,Аргумент = Неопределено)
Возврат Новый Структура("Тип,Парсер",Тип,Аргумент);
Конецфункции
Функция ПрименитьПарсер(Парсер, СтрокаАнализа)
ФункцияПарсера = СписокФункций [Парсер.Тип];
Если ФункцияПарсера = Неопределено Тогда
Вызватьисключение "Неизвестный тип парсера "+Парсер.Тип;
Конецесли;
Возврат Вычислить(ФункцияПарсера + "(Парсер,СтрокаАнализа)");
Конецфункции
Процедура Инициализация()
СписокФункций = Новый Соответствие;
ЗарегистрироватьФункцииПарсера ("match", "ПарсерОдногоСимвола");
ЗарегистрироватьФункцииПарсера ("seq", "ПоследовательностьПарсеров");
ЗарегистрироватьФункцииПарсера ("/", "ОперацияВыбор");
ЗарегистрироватьФункцииПарсера ("+", "ОдинИлиБолее");
ЗарегистрироватьФункцииПарсера ("*", "НольИлиБолее");
ЗарегистрироватьФункцииПарсера ("?", "Необязательно");
ЗарегистрироватьФункцииПарсера ("!", "НеПредикат");
ЗарегистрироватьФункцииПарсера ("&", "ИПредикат");
Конецпроцедуры
Процедура Тест_Последовательно() экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
ТестовыйПарсер = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА,БукваМ,БукваА));
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив("м","а","м","а");
Образец = РазобраноУспешно(МассивРезультата,"");
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[1], Значение(образец)[1]);
юТест.ПроверитьРавенство(Значение(Результат)[2], Значение(образец)[2]);
юТест.ПроверитьРавенство(Значение(Результат)[3], Значение(образец)[3]);
Конецпроцедуры
Процедура Тест_ОперацияВыбор() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваП = ПостроитьПарсер("match","п");
БукваА = ПостроитьПарсер("match","а");
БукваМилиП = ПостроитьПарсер("/",ВМассив(БукваМ,БукваП));
ТестовыйПарсер = ПостроитьПарсер("seq",ВМассив(БукваМилиП,БукваА,БукваМилиП,БукваА));
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
МассивРезультата = ВМассив("п","а","п","а");
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[1], Значение(образец)[1]);
юТест.ПроверитьРавенство(Значение(Результат)[2], Значение(образец)[2]);
юТест.ПроверитьРавенство(Значение(Результат)[3], Значение(образец)[3]);
Конецпроцедуры
Процедура Тест_ОдинИлиБолее_Один() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("+",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"ма");
МассивРезультата = ВМассив(ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
Конецпроцедуры
Процедура Тест_ОдинИлиБолее_Более() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваП = ПостроитьПарсер("match","п");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("+",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"),ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
юТест.ПроверитьРавенство(Значение(Результат)[1][0], Значение(образец)[1][0]);
юТест.ПроверитьРавенство(Значение(Результат)[1][1], Значение(образец)[1][1]);
Конецпроцедуры
Процедура Тест_ОдинИлиБолее_Ноль() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("+",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
Образец = РазобраноНеудачно("папа");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
Конецпроцедуры
Процедура Тест_НольИлиБолее_Ноль() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("*",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
МассивРезультата = новый Массив;
Образец = РазобраноУспешно(МассивРезультата,"папа");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат).Количество(), Значение(образец).Количество());
Конецпроцедуры
Процедура Тест_НольИлиБолее_Более() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("*",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"),ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
юТест.ПроверитьРавенство(Значение(Результат)[1][0], Значение(образец)[1][0]);
юТест.ПроверитьРавенство(Значение(Результат)[1][1], Значение(образец)[1][1]);
Конецпроцедуры
Процедура Тест_Необязательно_Есть() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("?",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив("м","а");
Образец = РазобраноУспешно(МассивРезультата,"ма");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
Конецпроцедуры
Процедура Тест_Необязательно_Нет() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("?",БукваМиА);
Результат = ПрименитьПарсер(ТестовыйПарсер,"папа");
Образец = РазобраноУспешно(Неопределено,"папа");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат), Значение(образец));
Конецпроцедуры
Процедура Тест_ИПредикат() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("seq",ВМассив(БукваМиА,ПостроитьПарсер("&",БукваМ)));
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"ма");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
Конецпроцедуры
Процедура Тест_НеПредикат() Экспорт
БукваМ = ПостроитьПарсер("match","м");
БукваА = ПостроитьПарсер("match","а");
БукваП = ПостроитьПарсер("match","а");
БукваМиА = ПостроитьПарсер("seq",ВМассив(БукваМ,БукваА));
ТестовыйПарсер = ПостроитьПарсер("seq",ВМассив(БукваМиА,ПостроитьПарсер("!",БукваП)));
Результат = ПрименитьПарсер(ТестовыйПарсер,"мама");
МассивРезультата = ВМассив(ВМассив("м","а"));
Образец = РазобраноУспешно(МассивРезультата,"ма");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0][0], Значение(образец)[0][0]);
юТест.ПроверитьРавенство(Значение(Результат)[0][1], Значение(образец)[0][1]);
Конецпроцедуры
Инициализация();
Так как скачивание файла стоит денег, то привожу код модуля полностью.
Запускаем тесты и видим что все в порядке. TDD — вполне приемлемый стиль разработки, как вы видите. Даже в 1С.
В принципе, этого набора парсеров нам достаточно, что бы реализовать парсер для любой грамматики, которая может быть описана PEG-парсером. Но прежде чем приступить к этому, давайте научимся расширять возможности парсеров.
4 Расширение возможностей парсеров
Страшным словом «расширение» на самом деле называется процесс разработки функции разбора и добавления ее в список зарегистрированных функций парсера.
4.1 Регистро-независимое сравнение символа
Первое расширение, которое мы реализуем — сравнение символа без учета регистра. Делается это следующим образом
Функция ПарсерОдногоСимвола_РегистроНезависимый(Парсер,СтрокаАнализа)
СимволАнализа = Сред(СтрокаАнализа,1,1);
Если НРег(СимволАнализа) = НРег(Парсер.Парсер) Тогда
Возврат РазобраноУспешно(СимволАнализа,Сред(СтрокаАнализа,2));
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
Конецесли;
Конецфункции
Обычно в регулярных выражениях для обозначения регистро-независимого поиска используется префикс «i». Не будем оригинальны, и добавим определения к нашему парсеру
ЗарегистрироватьФункцииПарсера ("imatch", "ПарсерОдногоСимвола_РегистроНезависимый");
Напишем тест для проверки —
Процедура Тест_imatch() экспорт
БукваМ = ПостроитьПарсер("imatch","м");
Результат = ПрименитьПарсер(БукваМ,"Мама");
Образец = РазобраноУспешно("М","ама");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат), Значение(образец));
Конецпроцедуры
4.2 Сравнение со строкой
Второе расширение, которое будет нам необходимо — сравнение со строкой. В принципе это и логично — большинство лексем, которые мы будем извлекать из текста запроса, длиннее одного символа. Определим парсер для оператора «match-string»
Функция ПарсерСтрокиПоОбразцу(Парсер,СтрокаАнализа)
количестоСимволов = СтрДлина(Парсер.Парсер);
строка = Сред(СтрокаАнализа,1,количестоСимволов);
Если строка = Парсер.Парсер Тогда
Возврат РазобраноУспешно(строка,Сред(СтрокаАнализа,количестоСимволов+1));
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
КонецЕсли;
КонецФункции
Добавим определение в инициализацию
ЗарегистрироватьФункцииПарсера ("match-string", "ПарсерСтрокиПоОбразцу");
Напишем тест для проверки —
Процедура Тест_match_string() экспорт
строкаОбразец = ПостроитьПарсер("match-string","select");
Результат = ПрименитьПарсер(строкаОбразец,"select 1");
Образец = РазобраноУспешно("select"," 1");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат), Значение(образец));
Конецпроцедуры
4.3 Регистро-независимое сравнение со строкой
Следующее расширение которое опишем – регистро-независимое сравнение со строкой. Определим парсер для оператора «imatch-string»
Функция ПарсерСтрокиПоОбразцу_РегистроНезависимый(Парсер,СтрокаАнализа)
количестоСимволов = СтрДлина(Парсер.Парсер);
строка = Сред(СтрокаАнализа,1,количестоСимволов);
Если НРег(строка) = НРег(Парсер.Парсер) Тогда
Возврат РазобраноУспешно(строка,Сред(СтрокаАнализа,количестоСимволов+1));
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
КонецЕсли;
КонецФункции
Добавим определение в инициализацию
ЗарегистрироватьФункцииПарсера ("imatch-string", "ПарсерСтрокиПоОбразцу_РегистроНезависимый");
Напишем тест для проверки —
Процедура Тест_imatch_string() экспорт
строкаОбразец = ПостроитьПарсер("imatch-string","select");
Результат = ПрименитьПарсер(строкаОбразец,"seLECt 1");
Образец = РазобраноУспешно("seLECt"," 1");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат), Значение(образец));
Конецпроцедуры
4.4 Интервал
Небольшой пример — пусть нам необходимо реализовать парсер для следующей грамматики
|
‹Number› |
::= |
‹Digits› [{‹dot› ‹Digits›}] |
|
‹dot› |
::= |
. |
|
‹Digits› |
::= |
‹Digit›+ |
|
‹Digit› |
::= |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Во многих лексических сканерах есть альтернативная форма для определения‹Digit›, а именно — интервал
|
‹Digit› |
::= |
[0-9] |
Определим такую функцию
Функция Интервал(Парсер,СтрокаАнализа)
мин = Парсер.Парсер.Минимум;
макс = Парсер.Парсер.Максимум;
символ = Сред(СтрокаАнализа,1,1);
Если мин
Добавим определение в инициализацию
ЗарегистрироватьФункцииПарсера ("range", "Интервал");
Напишем тест для проверки —
Процедура Тест_range() экспорт
цифра = ПостроитьПарсер("range",Новый Структура("Минимум,Максимум","0","9"));
число = ПостроитьПарсер("+",цифра);
Результат = ПрименитьПарсер(число,"1234");
Образец = РазобраноУспешно(ВМассив("1","2","3","4"),"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
юТест.ПроверитьРавенство(Значение(Результат)[1], Значение(образец)[1]);
юТест.ПроверитьРавенство(Значение(Результат)[2], Значение(образец)[2]);
юТест.ПроверитьРавенство(Значение(Результат)[3], Значение(образец)[3]);
Конецпроцедуры
4.5 Сравнение с кодом символа
Неочевидный парсер, который нам потребуется — сравнение с кодом символа. Дело в том, что символы с ASCII-кодом меньше 48 нельзя ввести с клавиатуры (точнее можно, если использовать Alt+код, но такую возможность не рассматриваем), но вот в тексте, прочитанном из файла, такой символ может встретиться. Опишем парсер и подключение к нашей системе
Функция СравнитьСКодом(Парсер,СтрокаАнализа)
Символ = Сред(СтрокаАнализа,1,1);
КодСимвола = КодСимвола(Символ);
Если Парсер.Парсер = КодСимвола Тогда
Возврат РазобраноУспешно(символ,Сред(СтрокаАнализа,2));
Иначе
Возврат РазобраноНеудачно(СтрокаАнализа);
КонецЕсли;
КонецФункции
Добавим определение в инициализацию
ЗарегистрироватьФункцииПарсера ("char-code", "СравнитьСКодом");
Напишем тест для проверки —
Процедура Тест_code() экспорт
//КодСимвола("1") = 49
цифра = ПостроитьПарсер("char-code",49);
число = ПостроитьПарсер("+",цифра);
Результат = ПрименитьПарсер(число,"1234");
Образец = РазобраноУспешно(ВМассив("1"),"234");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат)[0], Значение(образец)[0]);
Конецпроцедуры
4.6 Преобразование результата
Следующий неочевидный парсер, который нам потребуется — функция трансформации результата.
Для чего он нужен? Давайте рассмотрим парсер числа определенный чуть выше и посмотрим что этот парсер Тест_range() возвращает — как видим — это массив, но нужен ли он нам? В AST которое мы будем строить, вероятнее потребуется именно число, прочитанное из строки, а не последовательность прочитанных символов.
Давайте напишем такой парсер
Функция Преобразователь(Парсер,СтрокаАнализа)
_Парсер = Парсер.Парсер;
ФункцияПреобразования = _Парсер.Функция;
Результат = ПрименитьПарсер(_Парсер.Парсер,СтрокаАнализа);
Если Успех(Результат) Тогда
Значение = Вычислить(ФункцияПреобразования+"(Результат)");
Возврат РазобраноУспешно(Значение,Остаток(Результат));
КонецЕсли;
Возврат РазобраноНеудачно(СтрокаАнализа);
КонецФункции
Добавим определение в инициализацию
ЗарегистрироватьФункцииПарсера ("fn", "Преобразователь");
Напишем тест для проверки —
Функция РезультатВЧисло(результат)
строка = "";
для каждого х из результат.Значение Цикл
строка=строка+х;
КонецЦикла;
Возврат Число(строка);
КонецФункции
Процедура Тест_fn() экспорт
цифра = ПостроитьПарсер("range",Новый Структура("Минимум,Максимум","0","9"));
число = ПостроитьПарсер("+",цифра);
Преобразователь = ПостроитьПарсер("fn",Новый Структура("Парсер,Функция",число,"РезультатВЧисло"));
Результат = ПрименитьПарсер(Преобразователь,"1234");
Образец = РазобраноУспешно(1234,"");
юТест.ПроверитьРавенство(Результат.Тип, образец.Тип);
юТест.ПроверитьРавенство(Остаток(Результат), Остаток(образец));
юТест.ПроверитьРавенство(Значение(Результат), Значение(образец));
Конецпроцедуры
Конечно примитмвных парсеров можно придумать очень много, но все они будут описываться именно по этой схеме — Функция работы, регистрация в списке доступных парсеров, вызов
В следующей публикации будет рассмотрен вопрос как учитывать позицию символов, кэширование работы парсеров и как разработать простой парсер для языка запросов 1С.
Предложения по улучшению стиля/текста/примеров/алгоритмов вы можете направлять на мой почтовый ящик wwal@yandex.ru
Нифига себе генератор синтаксических анализаторов)))
Основательно
(2) главное что бы понятно было.
Плюс однозначно. Хотя описано довольно сложно. Надо будет потом хорошенько повникать, а то так с ходу и не понимаю )
(4) Ну может в следующей части, на реальном примере разбора текста запроса станет ясно. Хотя старался писать просто и мало. В основном все в коде
Статья интересная. Чем это может быть полезно на практике?
РосПозорНадзор запретил гитхаб в нашей стране, там нашлась статья, как неудачник может прекратить свои страдания.
(6) Обычно парсеры для разбора строк используются.
(7) Ну если тебе так сильно нужен будет исходный код — вышлю на мыло. Ну или пожди пока эту идиотскую блокировку снимут
Что-то уж очень запутано и много кода. К тому же конструктор запросов уже существует для управляемого приложения.
На мой взгляд для решения данной задачи проще использовать конечные автоматы или иерархические конечные автоматы. Из плюсов: алгоритм занимает «десяток» строк кода + таблица правил настраиваемых вручную (есть редакторы), которые можно поменять довольно быстро; графическое представление алгоритма.
А из минусов?
Я не понял, а внутри архива есть парсер запроса или только тесты?
Только тесты к тому что описано.
В следующей публикации будет рассмотрен вопрос как учитывать позицию символов, кэширование работы парсеров и как разработать простой парсер для языка запросов 1С.
(12)
А сам парсер будет? 🙂
Просто тесты бессмысленны без того, что они проверяют 🙂
(13) Ну а как же без этого? Будут. Правда сперва будет статья про расчет координат, затем обработка ошибок и восстановление состояния, потом кэширование.
А после — любой сможет написать свой парсер — с девами и вином.
(14)
Будет интересно посмотреть на конструирование парсера. Сам сейчас занимаюсь подобным — может чего почерпну.
Ну коли занимаешься — присоединяйся. Гуртом и батьку бить легче, не то что парсер писать 🙂
Это, кстати, ко всем желающим относиться — если есть идеи о чем стоит написать — говорите.
А ещё есть такая интересная штука, как теория формальных языков. Её в институтах изучают.
Также есть и практические инструменты, использующие эту теорию. Например классика — yacc и bison — для C/C++. Или GOLD Parser для всего остального.
Вот здесьhttp://infostart.ru/public/239061/ разбор выражения с классической зрения. Для ознакомления.
Второе — формальная грамматика для языка запросов в 1С — отдельная песня. Если посмотришь на реализацию от tormozit — всплакнешь. На остальные — вообще лучше не смотреть, даже моих трех коридоров церковно-приходской школы хватает понять — reduce/reduce конфликт это очень прикольно.
А насчет генераторов парсеров — в прошлом году выкладывал свой шаблон для GoldenParser — позволял по граматике сразу строить код 1С — основной вопрос был «что это и зачем?» Снес нафиг. А ты говоришь FSM, CFG….
(18)
Это про «Подсистема «Инструменты разработчика» v3.20″?
Если в двух словах? 🙂
(18)
Про ГолденПарсер что-то помню такое, что было. Код 1С — всмысле, байт-код?
Правильное название GOLD Parsing System, а не Golden Parser. Грамматика гибридного языка запросов (1с + sql) для этого парсера мной развивается тутhttp://devtool1c.ucoz.ru/load/grammatika_jazyka_zaprosov_1s_8_2_goldparser/1-1-0-4 . Она используется в подсистеме «Инструменты разработчика» при построении дерева запроса в консоли запросов и в собственном конструкторе запроса.
(18) да, в моей грамматике есть ряд кривых моментов, которые появились для обхода reduce/deduce конфликтов. Как говорится красиво сделал до туда, до куда осилил, а дальше залепил бреши. Самое главное, что на основе этой грамматике сейчас работает самый продвинутый из виденных мной конструктор запросов.
(23) Он не только самый продвинутый, но и единственный которым можно пользоваться. Тебе за него — отдельный респект и уважуха. В принципе если бы не проблемы с комментариями и определением ид это или имя функции — она бы и мне подошла. Но не срослось. Сергей, если обидел тебя своим высказыванием про «плакал» — прошу прощения, но высказывание относилось не к твоему труду, а к gold parser’у — (как к неособо дружелюбной системе разроботки), и КС грамматикам в принципе (перестал их любить)
(21) Нет, в код 1С (текст)
(20) Возьми ты ту же грамматика для языка запросов и попробуй встроить комментарий везде где он может быть — если делать будешь это в GP — то там тебе даже подсветят где происходит конфликт. И подробно разжуют почему. Вообще комментарии в грамматике — отдельная тема.
С трудом представляю какие могут быть сложности с комментариями.
Ссылку на грамматику тебе дали. Скачай и добавь поддержку комментариев. Проблему поймешь легко.
Вот пример
«ВЫБРАТЬ // хитрый комментарий 1
| 1 // хитрый комментарий 2
| КАК // хитрый комментарий 3
|Поле1 // хитрый комментарий 4
|// хитрый комментарий 5
|ОБЪЕДИНИТЬ // хитрый комментарий 6
| // хитрый комментарий
|ВСЕ // хитрый комментарий 7
|
|ВЫБРАТЬ // хитрый комментарий 8
| // хитрый комментарий 9
|2″
Если сделаешь — тебе спасибо очень многие скажут. 🙂
(26)
Я грамматиками не балуюсь — просто беру и пишу 🙂 Как-то пробовал вникнуть в него(голден парсер) — но что-то у нас характеры не сошлись.
Возможно это архитектурные ограничения Голден Парсера и там действительно это проблема. В моей текущей версии парсера комментарии легко обрабатываются. До запроса еще не дошел, но не думаю, что там возникнут проблемы.
Другими словами — ты пишешь то же самое что и я — нисходящий разбор. Интересно посмотреть — как без баловства с грамматиками можно что-то сделать.
> До запроса еще не дошел, но не думаю, что там возникнут проблемы.
При ручном разборе — нет. Не возникнут. Как впрочем и проблем с «|» не будет — у тебя оператор выбора — упорядоченный получается.
> Возможно это архитектурные ограничения Голден Парсера и там действительно это проблема.
Нет. В GP — не проблема, в общем случае для КС это сложно.
(24) Да, среда разработки GoldParser конечно отвратная. Видимо все потому, что она кроссплатформенная и потому интерфейс пользователя такой корявый и неудобный. Но альтернатив, таких же доступных для применения в 1С, я не видел. Еще есть такая крутая штука ANTLRhttp://www.antlr.org/ , но для него нет готового COM движка.
Да, в грамматике моей самая большая беда с идентификаторами. Я много времени убил на поиск красивого решения, но так и не нашел. Мне даже кажется, что его там в принципе нельзя сделать красивым.
И по комментариям тоже кривая реализация конечно, из-за чего я ее так долго и не делал, т.к. искал опять же красивое решение. В некоторых местах комментарии вызывают ошибку парсера, т.к. они лексемы рождают, которые несколько правил могут начинать. Но уже мой код в 1С там выдает подсказку пользователю, чтобы он переставил комментарии. И за разбор комментариев таким образом пришлось заплатить некоторым снижением гибкости расположения комментариев в тексте запроса.
(27) AlexanderKai,
Грамматика языка запросов 1с в несколько раз сложнее грамматики встроенного языка 1с и соответственно неоднозначных переходов к правилам при встрече лексемы комментария будет намного меньше.
> Мне даже кажется, что его там в принципе нельзя сделать красивым.
В той постановке задачи которую принял ты — да, лучше чем у тебя сделано — никак. Повторюсь — это проблема всех КС грамматик — прочитан символ и надо определиться куда дальше идти — если сверток больше 2 — конфликт. В PEG этого нет, так как оператор альтернативы — упорядочен. Кстати, если упростить твою грамматику до одного id, а выяснение роли этого идентификатора (идентификатор ил имя функции) вынести в интерпретатор — то грамматика упрощается.
Проблема с комментариями решается просто на уровне GP. Но тогда тебе придется очень много кода перекроить. При чтении комментария генерируется токен с типом комментарий (даже 2 — что бы различать многострочные и однострочные комментарии). Можно на это завязаться — но повторюсь — много переписывать придется.
>Грамматика языка запросов 1с в несколько раз сложнее грамматики встроенного языка 1с
Ну не сказал бы. В инете валяется грамматика которую еще MMF (по моему он, могу ошибаться) делал. Посложнее языка запросов будет.
(31)
Это я в первую очередь пробовал, но COM-объекты неизменяемые, т.е. я не могу в них вставлять свои дополнения (прилепить комментарий к текущему правилу). Поэтому такой способ кажется все очень усложнит.
Под сложностью грамматики имею ввиду некую функцию от количества правил и средней степени вложений правил. Мне кажется грамматика встроенного языка тут явно проще будет.
Мне чет институтские лекции по основам трансляторов вспомнились:) Автору респект.
Ищу соавтора на вторую часть статьи. Будут описано построение парсера для арифметических выражений, построение генератора парсера и в качестве примера использования — описана грамматика markdown-разметки для комментариев в коде. Желающие — отметьтесь в личке.
(34) не могу написать вам личное сообщение, потому что у меня менее 1 стратмани :))). Готов поучаствовать.
В снегопате процедура парсинга языка 1С состоит из 3 тысяч строк, в которых 1300 «goto» и ни одного «for» 🙂
Руками писал?
(37)
Нет конечно. Сначала bison, а потом мой скрипт по его результатам.
Косяк с х.Возврат обошел так же как и в 77?
(39)
Немного недопонял, какой именно косяк и как я его обошел в 77?
Использование ключевых слов в имени полей. В грамматике для 77 ты ввел отдельное правило, а здесь как сделал?
(41)
Показать
(26)
А зачем комментарии встраивать в грамматику? Может я что-то не понимаю? Но обычно комментарии еще на этапе лексического разбора убираются.
Показать
(43) orefkov, глянь на инструменты разработчика — по AST восстанавливается текст запроса.
(44)
А, я так понимаю это задача о «конструкторе запросов, оставляющем комментарии» ?
То есть залили текст, поредактировали дерево, вылили текст?
Имхо, это уже не задача грамматики.
Вернее, в терминах одной лишь грамматики строго не реализуема. Ибо грамматика — это прежде всего формальный подход, регламентирование, строгость и однозначность.
А какие формальные правила есть при написании комментариев? Нет, и быть не может. К примеру, стоит комментарий перед первым полем в списке полей. Он к чему относится? К конкретному полю:
«— Тут поле Товара»
А может ко всему списку полей:
«— Начинаем поля выборки»
А может просто
«— Вася 10.10.10 был и правил»
Поле «Товар» удалили — оставлять комментарий или нет?
Я бы все-таки и для этой задачи не вносил комментарии в грамматику. А сделал бы комментарий просто атрибутом узла AST. На этапе лексического разбора комментарий бы просто привязывал к ближайшей последующей/предыдущей значащей лексеме. В-принципе, при желании, после построения дерева (уже после парсера грамматики) можно было бы по нему пройтись и выделить комментарии в отдельные узлы. Но так и так — это уже задача семантики, а не грамматики — следующее звено цепочки «лексика-грамматика-семантика». Грамматика — не серебрянная пуля, и не надо стараться только на ней одной выехать.
Для того же телепата и снегопата ряд моментов мной был вынесен из грамматики на уровень семантики — ибо на уровне грамматики они решались сложно, а на уровне семантики — легко. К примеру «Возврат Выражение» — то, что это допустимо только в функции, но не в процедуре, что Прервать/Продолжить допустимо только в циклах, те же ключевые слова как имена свойств/методов — это уже делалось либо пост-парсером, либо хаком разбора.
(45) +1
В (29) я этой проблемы касался относительно комментариев. Была бы возможность (в COM-реализации парсера GoldParser), я бы ни за что не стал осквернять грамматику комментариями. Но кроме бесспорных минусов у варианта обозначения комментариев в грамматике есть и свои плюсы, которые конечно же не смогут перевесить минусы.
(46) tormozit, Была бы возможность (в COM-реализации парсера GoldParser), я бы ни за что не стал осквернять грамматику комментариями. — там же вроде в зависимости от того что вернет Parse() можно определить токен это или комментарий. могу ошибаться — сто лет туда не смотрел
(47) определять что лексер вернул комментарий, можно, но прикреплять к узлам синтаксического дерева эти комментарии напрямую нельзя. Дерево все полностью только для чтения.
Как использовать эти алгоритмы?
Это вот что?
‹тест1› ::= ‹м› ‹а› ‹м› ‹а›
‹м› ::= м
‹а› ::= а
с этим что делать?
Красиво изложенная теория не укладывается в голове.ни вдоль ни поперек.
Как подать на вход некий текст? Пример кода
Что будет на выходе?
Отсюда должно появиться понимание — Для чего все это?
Помню летал в вышмате в институте, но уже тогда кроме игр разума, не видел практической ценности этих знаний. Не показали, блин -)))
(49) Makushimo, вот буквально после грамматики ниже — процедура Тест_Последовательно() — в ней пример вызова для анализа строки и анализ полученного результата.
https://github.com/wwall/report-1/blob/master/md/part2.md — но опять же, повторюсь, для написания печатных форм и при подготовке к экзамену в 1С — никак не поможет.
Практическая ценность — конечно никакой. Одна игра разума. Возможно во второй части будет понятней —
(50)
тю на вас -))
я изучаю всякие подходы к реализации разного рода анализаторов/парсеров текстов
думал можт у вас научусь чему нибудь.
А тут ничего не понятно-)))
(50)
почему тест грамматики написан именно таким «убеймозг»-образом ??
это строка, которую можно подать на вход процедуре/функции?
например
СтрокаНаАнализ = «‹тест1› ::= ‹м› ‹а› ‹м› ‹а›
| ‹м› ::= м
| ‹а› ::= а»;
Результат = Тест_Последовательно(СтрокаНаАнализ);
Но в тексте статьи «вот буквально после грамматики ниже» ничего такого нету.
Там прямо в процедуре что-то происходит -))
ну то есть понять что с объектами происходит можно, но что это в итоге означает — нет.
Например, Образец получает результат разбора части строки.
а нечто с именем юТест имеет метод ПроверитьРавенство() который что-то делает.
и т.д.
(52) Makushimo, Там прямо в процедуре что-то происходит -))
Вы попутно с » всякими подходами к реализации разного рода анализаторов/парсеров текстов» язык программирования 1С еще изучите. Станет понятно что там происходит, почему и как.
И очень рекомендую прочитать часть озаглавленную «Необязательные инструменты использованные в процессе разработки» — станет ясно откуда берется юТест и почему код построен таким образом.
(53)
О, так еще и язык 1С учить надо? -)))
е-мае, куда я попал. -)))
короче на вас надежды нет, придется идти тем же, что и вы или другим путем.
и да, вы нереально круты раз написали такую непонятную крутую штуку. -))
За разрешение двигаться — тебе конечно спасибо. Я то думал что же меня так на месте держит?
Вот объясни что тебе не понятно??? Это же заготовка под PEG генератор. Что может быть проще??? Все ссылки объясняющие для чего это делается есть. Как это делается — в коде написано. Что будет сделано дальше — также написано.
А насчет непонятности — ты первый на 3 ресурсах кто об сказал. Может действительно стоит переписать.
С наступающим. Всех благ в новом году!
(55) ну когда уже ждать оформленного «тут» продолжения?
Тут действительно очень нелегко разобраться что к чему. Описания принципов сильно переплетено с деталями реализации. Описаны кирпичи, но нет какого-нибудь сарая, который можно из них построить. Примеры как разложить строку в массив символов и обратно или как «1234» преобразовать в 1234 мне совершенно не интересны. Мне интересно как из строки «128 + -65» получить число 63. При чем, желательно видеть где начало, а где конец. У вас же в тестах сложно увидеть вход и выход, они где-то в середине:
Показать
Где тут видно, что вход — «1234», а выход — 1234???
(57) sashocq, насчет такого сравнения прав — тут не очевидно, но в следующей версии поправлено.
Код
будет думаю более читаемым (но опять же — для этого придется взять мой правленый xUnit, с поддержкой этой функции).
>>> Мне интересно как из строки «128 + -65» получить число 63.
Пишу, вот честно — пишу продолжение, но сейчас увы времени мало. Да и отзывов на бета версию второй части тоже немного, что не добавляет оптимизма. 🙂
вселенская несправедливость, что тут до сих пор не было моего плюса.
Код большими кусками плохо воспринимается.
Делайте на такие вещи всё-таки функцию какую-нибудь, чтобы тут в статьях это была одна строчка, а не 5.
(Ушёл внимательно читать ещё разок…)
Нашел очепятку в параграфе 3.1 в пункте 1. прочатнный = прочитанный
В параграфе 4.4 исчезло окончание кода:
Спасибо за комментарии. В следующей версии будет все поправлено.
Вопрос снят
Фигасе ты заморочился )))
Давно это было. Сейчас бы делал совсем по другому.