Наверх

Следующая.  

 

Формы Бэкуса-Наура (БНФ).

 

 

            Как программируют трансляторы? Этот вопрос давно интересовал меня. Году в 1998 я, программируя функции для обработки строк, сам, «своим путем», продвинулся «на некоторую глубину» в этом направлении. Но потом пришел к выводу, что все-таки лучше сперва ознакомиться с тем, что наработано человечеством в этой области компьютерной науки. Далеко не сразу мне попалась литература, которую можно было нормально прочитать. То же относится и к статьям в Интернете. Причем, я бы не сказал, что мне попались отличные источники. Которые прочитал – и все понял, ничего больше не надо. Нет, то здесь, то там были рассеяны отдельные крупицы знаний, которые еще надо было перерабатывать и осмысливать своими мозгами. Однажды я скачал (и распечатал) даже исходник транслятора Бэйсика. Но заняться им сразу руки не дошли, а потом я с огорчением обнаружил, что потерял распечатку.

            Кстати, пока под транслятором я подразумеваю и компиляторы, и интерпретаторы.

            Оказалось, что в основе программирования трансляторов лежит целая математическая теория. Раздел не то дискретной математики, не то теории множеств. И главное здесь – это грамматики и формы Бэкуса-Наура.

            В математические определения, связанные с грамматиками, я здесь вдаваться не буду. Если вам интересно – прочитайте соответствующую книгу или статью. Скажу только, что прежде всего есть так называемый алфавит – некое множество символов. Из этих символов составляются некоторые сочетания (можно назвать их словами). А уже эти сочетания составляют текст (если это грамматика обычного человеческого языка) или программу (если это грамматика языка программирования). Текст может быть правильным (то-есть составленным по правилам этой грамматики) или неправильным. И основная задача теории – определить, правильный или неправильный этот текст. Правила «кодируются» с помощью своеобразных конструкций – форм Бэкуса-Наура.

            Попробую дать определение этих форм. Каждая форма – это некоторая строчка.

            Есть знак := . Он отделяет левую часть строки от правой.

            Есть так называемые «терминалы» - это, грубо говоря, то, из чего состоит язык. Допустим, человеческий язык состоит из предложений, предложения – из слов, слова бывают подлежащими и сказуемыми. Слова же в свою очередь состоят из букв. Предложения, слова, подлежащие и сказуемые, буквы – это все терминалы. Впрочем, буквы называют еще терминальными символами.

            Если же мы имеем строку арифметического выражения, то она состоит из множителей (или делимых-делителей), а также из слагаемых, а они в свою очередь состоят из чисел. Причем слагаемое само может состоять из слагаемых. Например:

            1+2+3

            Слагаемое 1+2 само состоит из двух слагаемых – 1 и 2. Это – важный пункт.

            Терминалы часто как-то «обзываются» - например «множ» или “factor” (что вообще говоря должно обозначать одно и тоже – множитель.

            И, наконец, есть «нетерминалы» - < и >. Они как скобки обрамляют некоторые терминалы.

            Есть еще знак | - он разделяет несколько альтернативных терминалов.

 

            Наверное, пока непонятно, но все нужно смотреть и постигать на примерах.

            Это все самый «простейший» вариант форм Бэкуса-Наура. Есть и усложнения. Допустим, некоторые конструкции (в правой части строки) можно взять в квадратные скобки ([ … ]). Это означает, что конструкция может отсутствовать (то-есть или присутствует один раз, или отсутствует). А если без таких скобок – значит присутствует строго один раз.

            Есть и фигурные скобки ({ … }). «Обрамленная» ими конструкция повторяется некоторое (возможно нулевое) количество раз.

            Встречал я и такие скобки: {/ … /}. Это означает повторение 1 или большее количество раз (то-есть ненулевое количество).

            Есть и другие разночтения в определениях форм. Бывает, например, что опускают нетерминалы < и >.

            Кстати, насчет скобок. Я бы ввел еще один вид скобок, означающий повторение определенное количество раз. Обозначение может быть таким:

            {5/ … /5} – повторение 5 раз. В некоторых случаях это может оказаться полезным.

 

            Теперь приступим к примерам.

1)      Дадим определение идентификатору с помощью БНФ (то-есть форм Бэкуса-Наура):

<Буква> := A|B|C|…|Z

<Цифра> := 0|1| … |9

<Идент> := <Буква> {<Буква>|<Цифра>}

 

Основная идея такого формализма понятна, я надеюсь. Идентификатор начинается обязательно с буквы, а дальше идет некоторое количество букв или цифр. В этом смысл вышеприведенных 3-х строк.

Примеры идентификаторов:

A   B  A1   B2C3   DA123

 

Отметим тут же, что всегда есть «самый главный» терминал (и, соответственно, главная строка, в левой части которой он находится), соответствующий тому объекту, который мы исследуем. В данном случае исследуем мы идентификатор, а самый главный терминал – это «Идент».

 

2)      Грамматика целых чисел без знака:

<число> := <цифра>|<цифра><число>

<цифра> := 0|1|2|…|9

 

Обратим внимание, что терминал «число» встречается в первой строке 2 раза. Это важный момент. В дальнейшем он приведет к появлению рекурсии (когда мы будем программировать).

 

            3)  Формула с плюсами и минусами без скобок.

<формула> := <число>|<формула><знак><число>

<знак> := +|-

 

Что такое число – это можно было бы продолжить, приписывая строки примера 2.

Можно было бы записать и такую форму для этого случая:

                        <формула> := <слаг> {<add_знак><слаг>}

<слаг> :=  <число>

<add_знак> := +|-

 

А если и числа, и переменные, то:

                        <формула> := <слаг> {<add_знак><слаг>}

<слаг> :=  <число>|<переменная>

<add_знак> := +|-

 

 

            4)  Формула с плюсами и минусами со скобками.

<формула> := <op>|<формула><знак><число>

<op> := <число>|(<формула>)

<знак> := +|-

 

Обратим внимание, что во второй  строке появились скобки. «Op» – это некоторый промежуточный терминал, введенный для удобства.

 

 

            5)  Формула с плюсами и минусами, а также с умножением и делением без скобок. Входят в формулу не только числа, но и переменные.

<формула> := <слаг> {<add_знак><слаг>}

<слаг> := <множ>{<mul_знак><множ>}

<множ> := <число>|<переменная>

<add_знак> := +|-

<mul_знак> := *|/

 

То-есть, самые крупные блоки, которые мы выделяем из строки с формулой – это слагаемые. Заметим, что этим определяется самый низший приоритет операций сложения-вычитания. Обратим внимание еще на то, что левое слагаемое уже не состоит из вложенных слагаемых, в отличие от правого.

 

 

6)      Формула с плюсами и минусами, с умножением и делением без скобок, а также с унарным плюсом-минусом (причем унарный знак может присутствовать перед числом или переменной лишь в единственном числе).

            <формула> := <слаг> {<add_знак><слаг>}

<слаг> := <множ_со_знаком>{<mul_знак><множ_со_знаком>}

<множ_со_знаком> := [<add_знак>]<множ>

<множ> := <число>|<переменная>

<add_знак> := +|-

<mul_знак> := *|/

 

 

            7)  Формула с плюсами и минусами,  умножением и делением и со скобками.

<формула> := <слаг> {<add_знак><слаг>}

<слаг> := <множ>{<mul_знак><множ>}

<множ> := <число>|<переменная>|(<формула>)

<add_знак> := +|-

<mul_знак> := *|/

 

            8)  Добавляем функции (встроенные) одного переменного (пусть это будут для примера sin и cos).

<формула> := <слаг> {<add_знак><слаг>}

<слаг> := <множ>{<mul_знак><множ>}

<множ> := <число>|<переменная>|(<формула>)|<Func>(<формула>)

<add_знак> := +|-

<mul_знак> := *|/

<Func> := sin|cos

 

 

 

Диаграммы Вирта.

 

Есть еще альтернативный подход к описанию правил грамматики. Это диаграммы Вирта. Вирт, кстати, это создатель языка Паскаль.

Описания того, что есть что в диаграммах Вирта, я нашел в Интернете. Но скажу честно, понимается это с трудом. Опишу вкратце на словах. Нечто, обведенное кружком – это терминальный символ. Есть еще нечто, заключенное в прямоугольник – это «постоянная группа терминальных символов, определяющая название лексемы, ключевое слово и др.». То-есть в кружок и прямоугольник заключаются терминалы (то, что мы имели ввиду под терминалами в формах Бэкуса-Наура).

Есть еще «нетерминальный символ, определяющий название правила». Это, по-видимому, то, что в формах Бэкуса-Наура мы ставили в левой части строки, слева от знака «:=».

И есть еще соединительные линии. Они «обеспечивают связь между терминальными и нетерминальными символами». Эти линии расходятся, потом сходятся снова, проходя через кружки и прямоугольники. Иногда образуют циклические петли, касающиеся основной горизонтальной линии.

Диаграмма состоит из нескольких «рисунков», также, как БНФ – из нескольких строк. На одном рисунке всегда есть основная горизонтальная линия, в начале которой написано название правила. Это аналог одной строки из формы Бэкуса-Наура. Если далее линия раздваивается, то это аналог выбора одной из двух альтернатив в БНФ (напомню, что эти альтернативы разделяются знаком | ). Если же есть зацикленная линия (петля), касающаяся основной линии, то это аналог рекурсивной части строки Бэкуса-Наура, то-есть часть этой строки (справа) совпадает с левой частью строки. Если есть несколько петель (рядом или одна над другой), то их аналоги в БНФ будут разделятся знаком |.

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

<Буква> := A|B|C|…|Z

<Цифра> := 0|1| … |9

<Идент> := <Буква> |<Идент> <Буква>|<Идент> <Цифра>

 

Теперь – диаграмма из 3-х строк:

Отметим, что третью строку диаграммы можно нарисовать и так:

 

 

На мой взгляд, принцип построения диаграмм Вирта достаточно понятен. Хотя, возможно, что-то из теории я и упустил, поскольку изучал этот материал по довольно отрывочной информации.

 

Взаимное соответствие между формой Бэкуса-Наура и программой (то-есть программирование транслятора по заданной БНФ).

 

Теперь мы подходим к самому главному моменту. Как, имея форму Бэкуса-Наура, составить программу? Допустим, форму, соответствующую некоторому языку (языку арифметических формул, например), мы составили.

Правила соответствия БНФ-Program такие:

1)Каждому терминалу, стоящему слева от «:=» (по крайней мере, тому, который может состоять из нескольких символов – такие мы брали в угловые скобки), соответствует функция в общей программе. То-есть, уточняю, «мелким» терминалам (таким как знак сложения, допустим, или цифра) функцию можно не сопоставлять. Но таким, как <Формула>, <Множитель>, <Слагаемое> - соответствующую функцию сопоставлять нужно.

 

2)Каково же будет «наполнение» этих функций? То-есть их  внутренняя начинка? Когда мы идем по правой части строки формы (то-есть правее от «:=») слева направо и встречаем «крупный» терминал (<Множитель>, <Слагаемое>), мы записываем внутри нашей функции (которую сейчас мы заполняем операторами) соответствующий вызов функции. Далее – фигурным скобкам (повторению ноль или более раз) будет соответствовать оператор цикла while. Условием этого цикла скорее всего будет наличие знака математической операции, который стоит первым в скобке (опять таки вероятнее всего). Если же скобки квадратные (действие ноль или один раз), то им соответствует условный оператор if.

Если в правой части строки БНФ есть ряд частей, разделенных вертикальной палочкой (| - знак альтернативы), то каждой такой части должен соответствовать свой участок кода внутри нашей функции. Реализовать такую «архитектуру» можно с помощью if или if-else. Можно применять и другие способы

        

             3)Наша программа, которую мы транслируем, должна выполнять некоторую полезную работу, например складывать и перемножать числа,  чтобы получить конечное значение формулы. Насчет этого «пункта» в формах Бэкуса-Наура не говорится ничего. Но в программе-трансляторе эти куски кода должны быть обязательно. Можно дать совет вставлять их и испытывать методом проб и ошибок.

 

            4)Еще важный пункт – при движении по правой части строки мы переходим от одного терминала к другому. Внутри функции этому соответствует вызов сканера для получения следующей лексемы после очередного «внутреннего» вызова функции. Но нужно учесть, что некоторые вызовы сканера может оказаться удобным «прятать» внутри вызываемых внутренних функций, а не в теле самой функции (которую мы пишем  в данный момент). Итак, движению слева направо (по правой части строки) от одного терминала к другому соответствуют вызовы сканера в программе.

            Уточняю – сканер это функция или подпрограмма (функция программы-транслятора), которая просматривает весь текст программы (транслируемой) и делит его на так называемые лексемы. Лексема – это число, или имя переменной, или знак операции, или название оператора (if, while), или что другое (имя функции например). Обычно вызов сканера «поставляет» очередную лексему в программе.

 

Пример соответствия строки БНФ и функции (язык Си):

<expr> := <term>{<addop><term>} – это строка БНФ.

 

Теперь функция:

 

expr( )

{

term( );

while(*Lex==’+’) 

/* Обратите внимание на соответствие <addop> и проверки на наличие  знака  +  в условии оператора цикла */

{

NextLexema( ); /* Вызов сканера */

Term( );

}

}

 

            Кстати, вышеприведенные рассуждения относятся не только к программам на языке Си, но и к любым языкам, поддерживающим рекурсивные функции (а также операторы цикла и условные операторы, естественно).

Хостинг от uCoz