Лексинг/Парсинг "здесь" документы

Для тех, кто является экспертом по лексированию и анализу... Я пытаюсь написать серию программ в perl, которые будут анализировать мейнфрейм IBM z/OS JCL для самых разных целей, но я нахожусь в качестве препятствия в методологии. Я в основном придерживаюсь идеологии лексинга/синтаксического анализа, выдвинутой Марком Джейсоном Доминисом в "Perl верхнего порядка", но есть некоторые вещи, которые я не могу понять, как это сделать.

JCL имеет то, что называется встроенными данными, что очень похоже на документы "здесь". Я не совсем уверен, как лексиковать их в токены.

Макет для встроенных данных выглядит следующим образом:

//DDNAME   DD *
this is the inline data
this is some more inline data
/*
...

Обычно "*" после "DD" означает, что следующие строки являются встроенными данными, которые заканчиваются либо "/*", либо следующей допустимой записью JCL (начиная с "//" в первых двух столбцах).

Более продвинутые, встроенные данные могут отображаться как таковые:

//DDNAME   DD *,DLM=ZZ
//THIS LOOKS LIKE JCL BUT IT ACTUALLY DATA
//MORE DATA MASQUERADING AS JCL
ZZ
...

Иногда встроенные данные сами по себе являются JCL (возможно, для перекачки в программу или для внутреннего считывателя).

Но вот руб. В JCL записи составляют 80 байт, фиксированных по длине. Все прошлое столбец 72 (cols 73-80) является "комментарием". Кроме того, все, что следует за пробелом, которое следует за действительным JCL, также является комментарием. Поскольку я пытаюсь манипулировать JCL в своих программах и выплевывать его обратно, я бы хотел записать комментарии, чтобы сохранить их.

Итак, вот пример встроенных комментариев в случае встроенных данных:

//DDNAME   DD *,DLM=ZZ THIS IS A COMMENT                                COL73DAT
data
...
ZZ
...more JCL

Первоначально предполагалось, что я мог бы извлечь самый лексер в строку JCL и сразу же создать не-токен для cols 1-72, а затем токен ([COL73COMMENT ', $1]) для столбца 73 комментарий, если таковой имеется. Затем он передал бы следующему итератору/токенизатору строку из текста cols 1-72, за которой следует токен col73.

Но как бы я, вниз по течению, захватить встроенные данные? Я изначально полагал, что самый верхний токенизатор может искать "DD\* (, DLM = (\ S *))" (или тому подобное), а затем просто продолжать извлекать записи из подающего итератора, пока не попадет в разделитель или действительный стартер JCL ( "//" ).

Но вы можете увидеть эту проблему здесь... У меня не может быть двух верхних токенизаторов... либо токенизатор, который ищет комментарии COL73, должен быть вершиной, либо токенизатор, который получает встроенные данные, должен быть наверху.

Я полагаю, что perl-парсеры имеют одинаковую задачу, поскольку видя

<<DELIM

не обязательно является концом строки, за которой следуют данные документа. В конце концов, вы можете увидеть perl like:

my $this=$obj->ingest(<<DELIM)->reformat();
inline here document data
more data
DELIM

Как бы токенизатор/парсер знал, чтобы токенизировать ") → reformat();" а затем все еще захватывать следующие записи как есть? В случае встроенных данных JCL эти строки передаются как есть, cols 73-80 НЕ являются комментариями в этом случае...

Итак, все берут на себя это? Я знаю, что будет много вопросов, разъясняющих мои потребности, и я рад прояснить столько, сколько необходимо.

Заранее благодарим за помощь...

Ответы

Ответ 1

В этом ответе я сосредоточусь на heredocs, потому что уроки можно легко перенести в JCL.

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

Для анализатора мы рассматриваем введение в heredocs <<END в виде строковых литералов. Но лексер должен быть расширен, чтобы сделать следующее:

  • Когда происходит ввод heredoc, он добавляет терминатор в стек.
  • Когда встречается новая строка, тело heredoc лексируется, пока стек не будет пустым. После этого нормальный синтаксический анализ возобновляется.

Позаботьтесь об обновлении номера строки соответствующим образом.

В рукописном объединенном парсере/лексере это можно реализовать следующим образом:

use strict; use warnings; use 5.010;

my $s = <<'INPUT-END'; pos($s) = 0;
<<A <<B
body 1
A
body 2
B
<<C
body 3
C
INPUT-END

my @strs;
push @strs, parse_line() while pos($s) < length($s);
for my $i (0 .. $#strs) {
  say "STRING $i:";
  say $strs[$i];
}

sub parse_line {
  my @strings;
  my @heredocs;

  $s =~ /\G\s+/gc;

  # get the markers
  while ($s =~ /\G<<(\w+)/gc) {
    push @strings, '';
    push @heredocs, [ \$strings[-1], $1 ];
    $s =~ /\G[^\S\n]+/gc;  # spaces that are no newlines
  }

  # lex the EOL
  $s =~ /\G\n/gc or die "Newline expected";

  # process the deferred heredocs:
  while (my $heredoc = shift @heredocs) {
    my ($placeholder, $marker) = @$heredoc;
    $s =~ /\G(.*\n)$marker\n/sgc or die "Heredoc <<$marker expected";
    $$placeholder = $1;
  }

  return @strings;
}

Вывод:

STRING 0:
body 1

STRING 1:
body 2

STRING 2:
body 3

Параграф Marpa упрощает это, позволяя запускать события после разбора определенного токена. Они называются паузами, потому что встроенный лексик останавливает момент, чтобы вы взяли верх. Вот обзор высокого уровня и короткий блогпост, описывающий этот метод с демонстрационным кодом на Github.

Ответ 2

Если кто-то задавался вопросом, как я решил это решить, вот что я сделал.

Моя основная лексическая процедура принимает итератор, который набрасывает полные строки текста (который может взять его из файла, строки, что бы я ни захотел). Эта процедура использует это для создания другого итератора, который анализирует строку для "комментариев" после столбца 72, которая затем будет возвращаться в качестве маркера "mainline", а затем токена "col72". Этот итератор затем используется для создания еще одного итератора, который пропускает токены col72 без изменений, но принимает токены mainline и лексики их в атомарные токены (например, STRING, NUMBER, COMMA, NEWLINE и т.д.).

Но здесь основная проблема... в процедуре lexing есть ОРИГИНАЛЬНЫЙ ИТЕРАТОР... поэтому, когда он получает токен, который указывает, что есть документ "здесь", он продолжает обрабатывать токены, пока не попадет в токен NEWLINE (это означает конец фактической строки текста), а затем использует оригинальный итератор, чтобы снять данные документа. Поскольку этот итератор подает итератор атомных токенов, вытягивая его, он предотвращает распыление этих линий.

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

Easy-peasey.