Сергей Яковлев
Мой блокнот, мастерская, место где я делюсь своим опытом и мыслями

Язык языков. Часть вторая

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

META II

В 1963-1964 годах, буквально через несколько лет после появления BNF, за авторством Dewey Val Schorre работавшим тогда в Калифорнийском университете в Лос-Анджелесе (UCLA) выходит в свет язык программирования META II. Примечательно, что в 1963 году была разработана и стандартизована ASCII-таблица.

Основная сфера применения этого языка — создание компиляторов. В документации по языку, META II описывается как «синтакс-ориентированный язык для написания компиляторов», а так же «язык похожий на BNF». В этом языке были введены такие понятия и конструкции, которые в том или ином виде дожили и до наших дней в виде продолжения в EBNF и ABNF, о которых мы поговорим ниже.

В META II рекурсивное определение BNF было заменено циклом. Для этого была введена математическая группировка при помощи скобок $( и ). Фактически, следующее выражение:

$(foo)

следует понимать как ноль или более повторений foo. Например, для следующего BNF правила:

<expr> := <term> | <expr> + <term> | <expr> - <term>

мы должны были бы написать что-то вроде:

expr = term
       $( '+' term .out('ADD')
        / '-' term .out('SUB'));

На что тут следует обратить внимание:

  • Символы целевого языка (терминалы), стали определяться с помощью строк в одинарных кавычках (здесь это + и -)
  • Были удалены угловые скобки, что наложило ограничения на допустимый набор символов
  • Металингвистическая связка ::= была заменена на =
  • Для указания выбора используется наклонная черта (/) вместо вертикальной (|)
  • Правило заканчивается точкой с запятой (;)
  • Отныне пользователю предлагались уравнения, вместо металингвистических правил, как это называлось в BNF

Рассмотрим шаг за шагом как работает грамматика, приведённая выше.

Выражение оценивается слева направо. Первое, что проверяется это term. Если сопоставление с term не удалось, всё выражение откидывается и анализатор двигается дальше. Если сопоставление оказалось успешным, анализатор входит в $( … ). Затем анализатор пытается протестировать +. В случае неудачи срабатывает альтернатива (/) и анализатор оценивает -. Если хоть один из вариантов сопоставления с образцом оказался успешным, анализатор пытается провести сопоставление с тем term, который находится по правую сторону от знака. Если оба варианта сопоставления не удались, т.е. вторые term не удалось сопоставить, результат сопоставления остаётся успешным только для самого первого term, который мы видим сразу же после знака =. Тут всё достаточно просто. А теперь самое интересное. Так как оператор $( ) означает цикл из нуля или более итераций, то после успешного сопоставления со вторым term, цикл повторяется до тех пор, пока сопоставление не провалится. Так решалась задача рекурсивных определений BNF.

Любопытные штуки типа .out('ADD'), как мог догадаться читатель — служебный код, печатающий т.н. опкоды в стандартный вывод. Работало это так: если сопоставление удалось полностью, например - term, то печатаем SUB.

Мне показался забавным тот факт, что в документации указанно об обязательной точке с запятой в конце каждого уравнения, однако в самой документации, вместо этого используется точка, за которой сразу идёт запятая («due to keypuch limitations»). Вспомнилось, что в отчёте ALGOL 60 таких проблем не было. Там всё решалось ручным написанием всех нужных закорючек.

Если бы мы хотели как-то обозначить синтаксический элемент необязательным, в META II нам предлагалось использовалось служебное слово empty:

subsecondary = '*' primary / empty  ;
   secondary = primary subsecondary ;

Первую строку следует понимать так: либо символ *, за которым идёт нетерминал primary, либо ничего.

Кроме того, в META II появилось три типа данных: идентификаторы (обозначались через id), строки (обозначались через string) и числа (обозначались через number). Теперь давайте рассмотрим следующий пример целевого языка:

A
A + B
A + B * C
(A + B) * C

Это выдуманный алгебраический язык, с приоритетом операторов и группировкой. Если бы мы хотели покрыть этот язык грамматикой используя META II, то нам понадобилось бы написать следующий набор уравнений:

expr3 = id / '(' expr1 ')'        ;
expr2 = expr3 ('*' expr2 / empty) ;
expr1 = expr2 ('+' expr1 / empty) ;

Это в действительности не сложная грамматика. И если присмотреться, то всё становится достаточно очевидным, кроме одного понятия, с которым мы ещё не сталкивались — группировка. Только на первой строке скобки обрамлены кавычками, указывая на принадлежность скобок к символам целевого языка. Группировку скобками на второй и третьей строке следует расценивать так — сопоставление с группой считается успешным только в случае полного сопоставления с каждой альтернативой внутри группы.

Если BNF был формальным языком, не привязанным к каким либо реализациям и вычислениям, то в META II мы видим как ситуация стала меняться.

Остальной синтаксис META II не сложнее чем то, что я уже показал, а сам язык обладает богатыми возможностями. Но надолго останавливаться на нём мы не будем. Я лишь хотел показать некоторые черты, которые мы рассмотрим далее.

Позднее, уже в 1970-ых, появилась небезызвестная утилита yacc, использующая BNF продукции очень похожие не те, которые использовались в META II. yacc чаще всего используется в качестве генератора LALR-парсеров, и корнями, очевидно, уходит в BNF. Когда вы где-то слышите или читаете, что yacc использует BNF, то знайте — только от части, оригинальный BNF был совсем другой.

EBNF нотация

EBNF (Extended BNF) является расширенной версией BNF нотации. По существу является коллекцией дополнений в классическую форму BNF и отличается от BNF более ёмкими конструкциями, позволяющими при той же выразительной способности упростить и сократить в объёме описание.

Годом официального утверждения EBNF принято считать 1996. Стандартизирующая организация — ISO. В истории появления этой нотации не всё гладко. Были возражения на счёт того, что к 1996 году только ленивый не делал свои версии нотаций и появление ещё одной, под названием EBNF ничего не решало. Мол даже сама организация ISO не везде использовала EBNF. Но мы не будем на долго останавливаться на обсуждении этих идей, а рассмотрим основные отличия от BNF.

К чисто стилистическим изменениям стоит отнести следующие меры, ставшие популярными за 30 лет модификаций и заимствований у оригинальной нотации BNF:

  • Символы целевого языка, стали определяться с помощью строк в кавычках (META II)
  • Были удалены угловые скобки у нетерминалов (META II)
  • Металингвистическая связка ::= была заменена на = (META II)
  • В EBNF всё, что находится между символами (* и *) считается комментарием
  • Символ ; или . сигнализирует об окончании правила (META II)

Но наиболее значимые на мой взгляд изменения были произведены в самой структуре грамматических определений. И вот некоторые из них:

Опциональные элементы

Мы видели как это делается в META II. Так что EBNF не является первооткрывателем этой концепции. Для того чтобы указать опциональные части в грамматике, в EBNF использует квадратные скобки ([ и ]). Правило ниже говорит о том, что знак минус не является обязательным:

term = [ "-" ] factor ;

Например, если значения factor являются десятичными числами, то вот некоторые допустимые значения term:

-1
0
123123
-987987

Повторения

Идея рекурсивных определений в EBNF похожа на аналогичную идею в META II. Для обозначения того факта, что объект (или группа объектов) может повторяться ноль или более рез, в EBNF используются фигурные скобки ({ и }). Под объектом здесь имеется ввиду терминал (символ целевого языка) или нетерминал. Давайте посмотрим как это работает:

args = arg { "," arg } ;

Это правило определяет некоторый список аргументов, разделённый запятыми. Например, если arg является последовательностью ASCII-символов в диапазоне 65-90, то вот некоторые допустимые значения args:

HOST, PORT, DATABASE
NAME
A, B, C, D

Рассмотрим ещё один пример. Предположим у нас есть язык со следующим способом декларации функций:

function connect (string host, port, string password)

Здесь мы имеем некоторый выдуманный язык, где тип аргумента может быть не указан (здесь — port). Для определения грамматики аргументов функции мы могли бы составить следующие правила:

       args = maybe typed { "," maybe typed } ;
maybe typed = [ type ] arg ;

Как уже было отмечено ранее, угловые скобки в EBNF были убраны. Здесь наглядно показано с чем может возникнуть сложность у неподготовленного читателя (maybe typed это два нетерминала или один?).

Группировка

Я уже показывал выше, как группировка была введена в META II. Давайте разберёмся, как она представлена здесь. Для того, чтобы описать грамматику арифметической операции в BNF мы писали что-то вроде этого:

<expr> ::= <term> <op> <expr>
  <op> ::= - | +

В EBNF допустим такой формат, однако если откинуть стилистическую разницу, то гораздо лаконичнее записывать это так:

expr = term ( "-" | "+" ) expr ;

Конкатенация (последовательности)

Под конкатенацией следует понимать упорядоченный список (последовательность) синтаксических термов, разделённых запятой. Правило ниже описывает некоторое присваивание, за которым идёт точка запятой, а следом пробельный символ. Именно в такой последовательности:

end assignment = { assignment, ";", white space } ;

На этом я позволю себе завершить краткий обзор отличительных черт. Документации по EBNF много, чего не скажешь о BNF или META II. Для дальнейшего изучения предлагаю начать со следующих ссылок:

ABNF нотация

К 1977 году Arpanet использовала несколько неформальных стандартов для текстовых сообщений, отправляемых между ее хост-компьютерами. Было сочтено необходимым систематизировать эти методы и обеспечить те функции, которые казались неизбежными. Результатом этих усилий стал стандарт «Standard for the Format of ARPA Network Text Message» (RFC733), а затем обновлённый и дополненный в 1982 году (RFC822). В этих документах описывается модифицированная версия BNF нотации, которая была целесообразна для использования в Arpanet в те годы. И в документах, которые использовали эту модифицированную версию нотации, она называлась «Augmented Backus-Naur Form».

Сложно по одним лишь RFC судить когда всё это началось, потому что не ясно, в каких ещё работах и когда использовалась нотация ABNF. К этому времени, уже существовало множество модификаций BNF. Крупные проекты, связанные с разработкой языков программирования, протоколов сообщений и форматов всяческих данных просто делали себе очередную модифицированную версию. В дальнейшем, в Arpanet была предпринята попытка стандартизировать и систематизировать собственные изменения внесённые в оригинальную версию BNF. Результатом этой работы в 2008 году стал стандарт «Augmented BNF for Syntax Specifications: ABNF» (RFC5234).

Однако следует заметить, что к этому времени ABNF нотация уже использовалась в Arpanet на протяжении десятков лет. EBNF, как мы помним, появился аж в 1996 году, что примерно на 20 лет позже первых фактов использования ABNF. Многие RFC имели в своём определении копию определения ABNF. Выглядело это так: «вот документ, в котором мы описываем некоторые понятия, но в начале документа язык, который мы тут будем использовать». И спустя годы появился официальный документ, описывающий стандарт.

ABNF очень похож на EBNF, за исключением обозначения выбора, опциональных элементов и повторений. Рассмотрим основные отличия.

Имена нетерминалов определены с помощью регулярного выражения \b[A-Za-z][-A-Za-z0-9]+\b. А использование пробельных символов, как это было в первоначальной версии BNF, не допускается. В EBNF напомню пробельные символы в определении синтаксических термов допускались.

Заключение нетерминалов в угловые не является обязательным, но они могут использоваться вокруг нетерминала всякий раз, когда их наличие облегчает распознавание использование имени нетерминала. Для указания выбора используется наклонная черта (/) вместо вертикальной (|). Опциональные элементы обозначаются с использованием символов [ и ]. Символы целевого языка, стали определяться с помощью строк в кавычках. Определены

Для указания повторений используется символ *. Для указания того факта, что элемент или группа элементов должны повторяться n раз используется синтаксис n*. А для указания повторений от n до m раз используется синтаксис n*m. Например следующее определение в EBNF форме:

{ expansion }

в ABNF записывается так:

*( expansion )

Или например, если у нас есть правило описывающее число:

DIGIT =  %x30-39

то, для указания двузначного числа, мы могли бы использовать 2DIGIT.

Для более полного понимания отличий ниже приводится определение даты и времени с использованием ABNF формы. Данный пример получен с RFC5322.

date-time       =   [ day-of-week "," ] date time [CFWS]
day-of-week     =   ([FWS] day-name) / obs-day-of-week
day-name        =   "Mon" / "Tue" / "Wed" / "Thu" /
                    "Fri" / "Sat" / "Sun"
date            =   day month year
day             =   ([FWS] 1*2DIGIT FWS) / obs-day
month           =   "Jan" / "Feb" / "Mar" / "Apr" /
                    "May" / "Jun" / "Jul" / "Aug" /
                    "Sep" / "Oct" / "Nov" / "Dec"
year            =   (FWS 4*DIGIT FWS) / obs-year
time            =   time-of-day zone
time-of-day     =   hour ":" minute [ ":" second ]
hour            =   2DIGIT / obs-hour
minute          =   2DIGIT / obs-minute
second          =   2DIGIT / obs-second
zone            =   (FWS ( "+" / "-" ) 4DIGIT) / obs-zone

Заключение

В заключении хочу сказать, что металингвистические возможности, позволяющие определить грамматику формально, были у множества языков, включая COBOL и FORTRAN 77. Кроме того, существует множество других нотаций, с разной степенью популярности. Однако наиболее распространёнными являются нотации из BNF семейства.

P.S. Функционал комментариев я пока не сделал, поэтому если у вас есть вопросы или комментарии — используйте контактную фому, я отвечу.