[Перевод] Как сделать грамматику в Perl 6

Грамматика в программировании — это набор правил для разбора текста. Это очень полезная вещь — к примеру, грамматику можно использовать для проверки того, подчиняется ли строка текста конкретным стандартам или нет. У Perl 6 есть встроенная поддержка грамматик. Их настолько просто создавать, что единожды начав, вы обнаружите, что используете их везде.

В последнее время я работал над Module: Minter, простым приложением для создания базовой структуры модуля Perl 6. Мне надо было проверить, что предлагаемое имя модуля соответствует стандартам именования Perl 6.

Имена модулей — это идентификаторы, разделённые двумя двоеточиями. Идентификатор должен начинаться с алфавитного символа (a-z) или подчёркивания, за которым могут идти алфавитно-цифровые символы. Правда, у некоторых модулей может быть только один идентификатор, без двоеточий, а у других их может быть много (HTTP: Server: Async: Plugins: Router: Simple).

Определяем грамматику


В Perl 6 грамматики строятся на регулярках. Мне нужны две: одна для идентификаторов, другая — для разделителей в виде двоеточий. Для идентификаторов я задал:

<[A..Za..z_]> # начинается с буквы или подчёркивания
<[A..Za..z0..9]> ** 0..* # ноль или больше алфавитно-цифровых

Помните, что мы используем регулярки из Perl 6, и тут всё выглядит несколько по-другому. Класс символа определяется <[… ]>, а диапазон определяется оператором… вместо тире. Эта регулярка совпадает с любой первой буквой или подчёркиванием, за которым идёт ноль или более алфавитно-цифровых символов.
С двумя двоеточиями всё проще:

\:\: # пары двоеточий

Грамматики определяют при помощи ключевого слова grammar, за которым идёт название. Назову-ка я эту грамматику Legal: Module: Name

grammar Legal::Module::Name
{
  ...
}

Теперь можно добавлять в неё токены-регулярки:

grammar Legal::Module::Name
{
  token identifier
  {
    # первый символ -  буква или _ 
    <[A..Za..z_]>
    <[A..Za..z0..9]> ** 0..*
  } 
  token separator
  {
    \:\: # пары двоеточий
  }
}

Каждой грамматике нужно задать токен TOP, который обозначает её начало.

grammar Legal::Module::Name
{
  token TOP
  { # идентификатор, за которым идёт ноль или более пар separator - identifier 
    ^  [] ** 0..* $
  }
  token identifier
  {
    # первый символ -  буква или _ 
    <[A..Za..z_]>
    <[A..Za..z0..9]> ** 0..*
  } 
  token separator
  {
    \:\: # пары двоеточий
  }
}

Токен TOP определяет, что разрешённое имя модуля начинается с токена identifier, за которым идут ноль или больше пар токенов separator и identifier. Поддерживать такую штуку очень просто — если б я захотел изменить правила так, чтобы разделители содержали тире, я бы обновил регулярку только в одном токене.

Использование грамматики


Метод parse прогоняет грамматику на строке, и в случае успеха возвращает объект match. Следующий код обрабатывает строку $proposed_module_name, и либо выводит объект match, либо сообщение об ошибке.

my $proposed_module_name = 'Super::New::Module';
my $match_obj = Legal::Module::Name.parse($proposed_module_name);

if $match_obj
{
    say $match_obj;
}
else
{
    say 'Да что ж это за имя модуля-то такое, а?!';
}

Вывод:

「Super::New::Module」
 identifier => 「Super」
 separator => 「::」
 identifier => 「New」
 separator => 「::」
 identifier => 「Module」

Извлекаем содержимое объекта match

Можно не вываливать всё содержимое объекта match, а извлечь сыгравшие токены. В следующем коде используются именованные регулярки и ключи хэшей.

say $match_obj[0].Str; # Super
say $match_obj[1].Str; # New
say $match_obj[2].Str; # Module

say $match_obj; # все три

Action Classes (классы действий)


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

class Module::Name::Actions
{
  method TOP($/)
  {
    if $.elems > 5
    {
      warn 'В имени модуля слишком много идентификаторов – может, укоротишь?.. ';
    }
  }
}

Обычное такое определение класса в Perl 6. Я добавил метод TOP, совпадающий с первым токеном грамматики. Затем я подсчитываю количество совпадений, и если их больше 5, выдаю предупреждение. Выполнение оно не прерывает, но даёт понять пользователю о том, что стоит задуматься над переименованием модуля.

Затем инициализируем класс действий и передадим его в parse как аргумент:

my $actions = Module::Name::Actions.new; 
my $match_obj = Legal-Module-Name.parse($proposed_module_name, :actions($actions));

Грамматика вызывает соответствующий метод класса действий каждый раз, когда при парсинге встретится подходящий токен. В нашем случае это произойдёт один раз во время парсинга.

Грамматики в Perl 5


И в Perl 5 можно делать грамматики. Для схожего с Perl 6 решения можно посмотреть в сторону Regexp: Grammars или Ingy Döt Net’s Pegex. Отличные реализации можно посмотреть в главе 1 «Mastering Perl» от brian d foy, где содержится пример грамматики для JSON.

© Habrahabr.ru