Для обоснования таких структур нам потребуется определение их синтаксиса. Первое представление структур управления использовало для их описания естественный язык, как например: "Условный оператор начинается ключевым словом if, за которым следует…". Такой стиль полезен для первого знакомства, но не позволяет задать общий способ спецификации – он многословен и недостаточно точен. Нам нужно нечто обратное – сжатость определения и математическая строгость.
Таким требованиям отвечает
Мы видели, что полное описание языка программирования включает три уровня: лексический, синтаксический, семантический.
История языков программирования началась в пятидесятые годы прошлого столетия. Первым языком, получившим широкое распространение, стал язык Фортран (FORTRAN – FORmula TRANslator), предназначавшийся для научных вычислений и спроектированный командой из фирмы IBM под руководством Джона Бэкуса в 1954 году. В 1956 году для него был разработан компилятор, что предопределило успех и послужило толчком к созданию множества языков программирования.
Вскоре американские и европейские группы объединили усилия для проектирования международного стандарта языка, ставшего известным в 1956 году под именем
При подготовке спецификаций обнаружилась необходимость лучшего способа описания синтаксиса, чем тот неформальный подход, который использовал Джон Бэкус при описании Фортрана. К этому времени Джон Бэкус входил в состав рабочей группы, создававшей язык
С тех пор было предложено много различных вариаций
(рис 2.1) |
(рис 2.2) |
Для наших целей язык – это множество "предложений", каждое из которых задается конечной последовательностью лексем из некоторого "словаря". Например, простейшим правильным предложением языка Eiffel является текст класса:
class A end
Это предложение состоит из трех лексем: двух ключевых слов и идентификатора. Тексты, встречающиеся на практике, – тексты полезных классов – имеют значительно больше лексем.
Не каждая последовательность лексем из словаря языка является предложением этого языка: переставив лексемы end A class, мы не получим текст, задающий описание класса. Синтаксис языка и определяет, какие последовательности лексем являются предложениями языка, а какие нет. Спецификация синтаксиса называется грамматикой.
Грамматикой языка называется конечное множество правил, позволяющих создавать на основе словаря языка последовательности лексем, такие, что:
Из определения следует, что любое предложение языка может быть выведено путем применения правил (утверждение 2) и что любой такой вывод дает предложение языка (утверждение 1).
Большинство языков потенциально бесконечно. Например, число возможных программ на Eiffel бесконечно. Эта теоретическая возможность не создает никаких практических проблем, во-первых, потому, что в нашей жизни мы можем иметь дело только с конечным множеством программ, но, что более важно, каждый текст класса – предложение языка Eiffel – является конечной последовательностью терминалов. Последовательность может быть очень длинной, но она не может быть бесконечной.
Конечное множество правил должно позволять порождать бесконечный язык, должно позволять, например, создавать описание всех возможных классов на языке Eiffel. Это опять-таки не должно нас беспокоить. Нам не нужны все возможные классы, нам нужны только те, что интересуют нас. Достаточно, что мы знаем, что правила способны породить описание каждого возможного класса.
Для описания грамматик будем использовать форму
class, if …) и специальные символы (точка, запятая …).Class, представляющее текст класса, и категория Conditional, представляющая текст условного оператора. Соглашение, принятое в Conditional.Conditional определяет форму любого условного оператора: вначале идет if, затем образец Каждая продукция определяет синтаксис образца категории через ограничители и другие категории. Вот пример продукции для категории Conditional:
Это правило говорит, что любой образец Conditional – любой условный оператор – состоит из ключевого слова if, ограничителя, за которым следует образец категории Then_part_list, за которым, возможно, следует образец категории Else_part, ключевое слово end завершает конструкцию. Квадратные скобки отмечают возможные конструкции, которых может и не быть. Категории Then_part_list и Else_part имеют собственные продукции.
Каждая продукция определяет одну категорию, стоящую слева от символа $$\triangleq$$, который читается как "по определению является". В правой части этого определения стоит
Idenifier (идентификатор) и Integer (целое), чьи образцы являются идентификаторами, такими как имя класса Preview, и целочисленные константы, такие как 34. Грамматика не определяет терминальные категории, их синтаксис определяется на более низком лексическом уровне.Причина отнесения некоторых категорий к терминалам и их определения вне грамматики – чисто прагматическая: эти категории имеют простую структуру, для которой мощь
На синтаксическом уровне (Identifier и Integer, имеют бесконечное множество возможных образцов.
Для любого языка особое значение имеет категория, описывающая структуру самого верхнего уровня; для Eiffel – это категория Class. Такой
Продукция, приведенная для категории Conditional, показывает, что
Conditional такими символами являются символ $$\triangleq$$ и квадратные скобки, сигнализирующие о возможности присутствия конструкции, заключенной в скобки.Conditional является синтаксической структурой, которая содержит подструктуру, такую как Then_part_list.Во избежание недоразумений нужно быть внимательным и отличать символы языка от символов
В цветном оригинале книги используются разные цвета для символов разного вида. В черно-белом варианте контекст позволяет понять, какому языку принадлежит символ. Символов
Термин "образец" (spacimen) на первых порах мог вызывать недоумение. Казалось бы, следовало использовать привычный термин "экземпляр" (instance), но этот термин уже занят – он описывает объекты. Экземпляр класса – это объект, появляющийся во время выполнения, образец класса – это текст, задающий текст некоторого конкретного класса.
Продукция определяет синтаксис одной категории. Она имеет следующую форму:
$$Construct\;\triangleq\;Definition$$В левой части продукции задается определяемая категория, а в правой – ее определение, выраженное в терминах категорий (терминалов и нетерминалов) и ограничителей. В зависимости от формы определения различают три вида продукций: конкатенацию, выбор и повторение.
Конкатенация – это продукция, перечисляющая последовательность из нуля и более категорий; они следуют в определенном порядке и некоторые из них, возможно, заключены в квадратные скобки, что и определяет их возможность отсутствия. Наша первая продукция для Conditional является таким примером:
Такая продукция задает, что каждый образец категории, стоящей слева, по определению состоит из последовательности (конкатенации) стоящих справа образцов, идущих в заданном порядке, с тем исключением, что "возможные" образцы могут отсутствовать.
Продукция "Выбор" перечисляет одну или несколько категорий, разделенных Instruction (оператор):
Продукция "Выбор" задает, что каждый образец категории, стоящей слева, по определению состоит в точности из одного образца, стоящего справа. В отличие от конкатенации, порядок следования категорий, разделенных вертикальной чертой, не имеет значения. В последнем примере продукция говорит о том, что оператор языка может быть условным оператором или оператором цикла и так далее, перечисляя все возможные виды операторов языка. При чтении продукции вертикальная черта воспринимается и произносится как "или".
Продукция "Повторение" перечисляет две категории, стоящие справа от символа определения: одну нетерминальную, которую следует повторить, другую – обычно терминальную, используемую как разделитель. Для примера определим категорию (составной оператор), которая задает последовательность операторов, разделенных символом точка с запятой:
Из этого определения следует, что образец составного оператора является последовательностью из нуля или более образцов операторов, каждый отделяется от следующего, если тот есть, символом "точка с запятой". В соответствии с этим правилом возможные образцы имеют вид:
inst1inst1; inst2inst1; inst2; inst3Здесь inst1, inst2, inst3 являются образцами операторов.
В продукции использованы новые
Такое продукционное правило для составного оператора с возможностью нулевого повторения означает, что допускается пустой составной оператор. Это может быть полезно в некоторых случаях, например, можно корректно писать:
if some_condition then [S1]
else
instruction_1
instruction_2
end
Здесь пустая then-часть законна, поскольку синтаксически здесь стоит составной оператор, пустой в этом конкретном случае. Этот пример можно переписать в более понятной форме:
if not some_condition then [S2]
instruction_1
instruction_2
end
Но первая форма может быть предпочтительнее, учитывая процесс обновления программы, когда then–часть, предположительно, будет заполнена позже.
При определении некоторых категорий однократное присутствие образца обязательно, но возможно его повторение. В этом случае вместо метасимвола * (ноль или более) используется другой Conditional, включая определения входящих в нее категорий:
Продукция "Повторение", используемая в определении категории Then_part_list, показывает, что для образца категории допустимы такие формы:
cond1 then inst1
— Один образец Then_part
cond1 then inst1 elseif cond2 then inst2
— Два образца Then_part
cond1 then inst1 elseif cond2 then inst2 elseif cond3 then inst3
— Три образца Then_part
Пример можно продолжать, поскольку число образцов в этой конструкции может быть произвольно большим. Заметьте, что категория Then_part_list, задающая список, не может быть определена как возможно отсутствующая. В определении категории Conditional после if должна следовать хотя бы одна Then_part.
В
Случай 3 предполагает, что
интерпретируются как одна продукция "Выбор":
$$A\;\triangleq\;Def1\;|\;Def2$$Варианты
Вышеприведенный пример определения категории А должен быть записан в
Вместе с несколькими нотационными соглашениями это правило стиля задает специфику
При написании определений языков я обнаружил, что это правило ведет к введению дополнительных понятий – нетерминалов, таких как Concat и Repet в последнем примере, а ранее Then_part_list, что в конечном итоге позволяет выработать более понятное описание языка.
Все, что нужно знать о
Второе применение не столь фантастично, как кажется с первого взгляда. Возможно, вам еще не скоро придется проектировать язык общецелевого применения – соперник таких языков, как С#, Java или Eiffel. Но программистам довольно часто приходится иметь дело с "малыми" языками. Всякий раз, когда приходится обрабатывать данные сложного формата, эти данные можно рассматривать как предложения некоторого языка, синтаксис которого удобно задать, используя
Третье приложение (построение анализатора) полезно при написании компиляторов и других инструментов, предназначенных для обработки программ, а в более общем случае – структурированных текстов. Одна из первых задач для таких инструментов – это реконструкция структуры текста в форме абстрактного синтаксического дерева. Это работа анализатора, как мы увидим в следующей лекции. Любому анализатору необходимо формальное описание синтаксиса языка – он может получить его из
Вторая точка зрения на практике менее полезна, но в то же время важна. Давайте исследуем ее немного глубже. Для порождения всех возможных образцов любого
| Р1 | Для конкатенации – породить все возможные последовательности образцов перечисленных категорий, учитывая, что категории со статусом "возможные" могут как присутствовать, так и отсутствовать. |
| Р2 | Для повторения – породить все последовательности из нуля и более образцов (одну или более для знака +) указанной категории с заданным разделителем элементов последовательности. |
| Р3 | Если на любом из предыдущих шагов встретился |
| Р4 | Для выбора применяйте предыдущие шаги ко всем перечисленным категориям и собирайте все образцы, порожденные каждой из категорий. |
Эти четыре механизма порождения предложений применяйте до тех пор, пока хоть одно из них будет применимо. В конечном счете будут порождены все предложения языка. Процесс этот обычно не завершается, поскольку, как мы видели, языки, представляющие интерес на практике, являются бесконечными.
Применяя этот процесс к нетерминалу А, чья продукция использует В, возможно, придется применять те же правила – на шагах Р3 и Р4 – к другим категориям.
Последнее наблюдение может вызывать некоторое опасение. Что, если, применяя процесс к А, мы должны будем применить его к В, а это приведет к тому, что мы снова встретим А? Продукция для составного оператора является хорошим примером:
Определение включает категорию Instruction, продукция для которой включает категорию $$Compound :\;\triangleq$$
Заметьте, что и определение категории Conditional включает категорию . Если попытаться понять структуру , разыскивая его образцы путем применения вышеприведенных правил, то кажется, что мы впадем в цикл – вывод, не имеющий смысла.
Определение понятия, в котором явно или неявно понятие определяется через само себя, называется рекурсивным. Рекурсия – использование рекурсивных определений – самая популярная вещь во всех областях программирования, и мы посвятим ей отдельную лекцию. Но уже здесь, где нет практически полезных грамматик, не использующих рекурсию, мы убедимся в важности рекурсивных грамматик.
Начнем изучение с небольшого примера. Рассмотрим мини-язык с тремя ключевыми словами heads, tails, stop (головы, хвосты и остановка). Других терминалов в этом языке не будет. Начальным нетерминалом будет категория Game, и вся грамматика задается тремя продукциями – выбором и двумя конкатенациями:
Ситуация напоминает ситуацию с категориями Conditional, Instruction, , определяемыми друг через друга.
Game, принадлежащие языку, который порожден этой грамматикой? Что является образцами категорий Head_start и Tail_start? Постарайтесь дать ответ прежде, чем продолжите чтение.Из-за рекурсии грамматика может показаться бессмысленной. Но будем Game одна из ветвей, stop, является терминальной, что позволяет сгенерировать первое предложение (образец Game):
stopНо теперь мы можем использовать полученную информацию для генерирования образцов Head_start и Tail_start, определяемых через Game. Соответствующие продукции скажут нам, что heads stop является образцом Head_start, а tails stop – образцом Tail_start. Воспользуемся этими образцами в продукции для Game, получим два новых образца:
heads stoptails stopПолучив эти образцы, и применяя тот же процесс, можно получить следующее множество образцов:
heads heads stopheads tails stoptails heads stoptails tails stopЭтот процесс удвоения образцов можно продолжать. Становится понятным и общая конструкция образца Game: он представляет последовательность "голов" и "хвостов", идущих в произвольном порядке, она заканчивается терминалом stop. Нетрудно доказать, что любая такая последовательность является образцом. Немного сложнее доказательство того, что образцов другого вида для Game нет.
Очень простой язык этой грамматики с начальным нетерминалом Game можно рассматривать как все последовательности возможных исходов бросания монеты при игре в "орел или решка". Последовательность заканчивается в тот момент, когда игрок кричит stop. Этот язык можно описать и не рекурсивной грамматикой:
Грамматика задана тремя продукциями, первая из которых является конкатенацией, вторая – повторением с пустым разделителем, третья – выбором.
Применяя для генерирования языка продукционные правила, мы использовали стратегию, в которой терминалы предпочтительнее нетерминалов. Выбрав другую стратегию, можно впасть в бесконечный цикл, не сгенерировав ни единого предложения. Например, начав с первой возможности для Game, получим Head_start, а после его замены – heads Game. Многократно применяя эту же стратегию для Game, получим последовательность, в которой всегда присутствует Game, что не дает создать предложение языка, которое по определению состоит только из
Во избежание подобных ситуаций процессу генерирования языка необходимы подходящие стратегии, или эвристики, подобные той, которая применялась для выбора – если есть ветвь, содержащая только терминалы, то выбирается такая ветвь, в противном случае выбирается продукция, начинающаяся с лексемы.
Даже при наличии таких эвристик процесс генерации не создаст ни одного предложения языка, если его грамматика полностью рекурсивна. Для запуска процесса генерации необходимо, чтобы по меньшей мере некоторые выборы включали только лексемы. Грамматики, такие как
$$A\;\triangleq\;A$$или
$$A\;\triangleq\;B\\B\;\triangleq\;A$$бесполезны. Эти проблемы подробно будут обсуждаться в лекции, посвященной рекурсии. Более тонким случаем являются грамматики, содержащие лексемы, но являющиеся леворекурсивными, как в следующем примере:
$$Instruction\;\triangleq\;Compound\;|\;Assignment\\Compound\;\triangleq\;Instruction\;";"\;Instruction$$Для простоты в этой грамматике Assignment считается терминалом, определенным вне грамматики (по аналогии с лексемами, определяемыми на уровне
assignment_1assignment_1; assignment_2и так далее. Для получения конструктивного вида таких рекурсивных определений необходима общая теория, набросок которой будет дан позднее.
if, то соответствующий оператор может быть только условным (Conditional); если первая лексема – from, то оператор цикла и так далее.Синтаксис, который мы изучали в этой лекции, задавал конкретный синтаксис программы со всеми ее ключевыми словами, ограничителями и прочими деталями, которые играют важную синтаксическую роль, позволяя избежать двусмысленностей, но не несут никакой семантики. Ранее мы встречались с абстрактным синтаксисом, в котором эти детали отсутствовали, оставляя только те элементы, что несут за собой смысл.
Мы видели, как описать результирующую синтаксическую структуру, используя АСД (Абстрактное Синтаксическое Дерево), такое как ранее показанное дерево, задающее синтаксис нашего класса Preview1.
(рис 2.3) Абстрактное синтаксическое дерево
Как отмечалось, проще построить конкретное синтаксическое дерево, содержащее все символы исходного текста. Некоторые компиляторы так и поступают, но обычно в этом нет необходимости. Для последующих фаз компиляции, таких как семантический анализ, генерация кода и его оптимизация, синтаксические маркеры не играют роли. Все, что нужно для представления структуры программы, в точности содержится в АСД.
Если бы нашей целью было описание , точку с запятой можно было бы опустить, оставив просто Instruction .
В таких приложениях, как синтаксический анализ и компиляция исходных текстов, эта грамматика не принесла бы пользы, поскольку, очевидно, здесь требуется конкретная грамматика, обсуждаемая до сих пор. Но она может играть свою роль, помогая в изучении тех структурных свойств текстов, которые не зависят от деталей внешнего вида этих текстов.
Одно из приложений
Детальное рассмотрение процесса построения синтаксически управляемого компилятора или просто анализатора выходит за пределы данного курса. При желании можно познакомиться с идеями применяемых методов, изучая библиотеку EiffelParse. В этой библиотеке реализован не самый эффективный механизм разбора, но ее методы представляют понятную и практическую иллюстрацию применения ОО-принципов этой книги для построения анализатора и компилятора. Сам Eiffel использует более традиционные подходы разбора, с которыми можно ознакомиться, изучая библиотеку "GOBO".
Идея, стоящая за EiffelParse, состоит в том, чтобы строить нужные классы непосредственно по грамматике , CHOICE, (соответственно для продукций "Конкатенация", "Выбор" и "Повторение"). Например, для конкатенации класс будет просто перечислять различные компоненты, стоящие в правой части продукции, связывая каждую компоненту с классом, подобным образом описывающим конструкцию. Следует быть внимательным, имея дело с
Для разбора входного текста достаточно вызвать EiffelParse – процедуру parse для соответствующей категории. В результате для нее будет создано АСД. Затем можно добавить семантическую обработку любого типа, используя методы синтаксического класса. Этот подход демонстрирует мощь и элегантность ОО-моделирования процесса анализа и компиляции языка программирования.
Для терминальных конструкций, таких как идентификаторы и числа,
На синтаксическом уровне, покрываемом
Такие категории нетрудно выразить через
Для определения структуры лексических категорий, таких как в вышеприведенных примерах, мы можем использовать регулярную грамматику – упрощенную версию
Нетерминалами такой грамматики являются категории, подобные идентификаторам и целым, которые выступают в роли терминалов в
Каждая категория выражается как выбор между единичными символами, показанными в одинарных кавычках. Такие категории являются по-настоящему терминальными (атомарными), не подлежащими дальнейшим уточнениям. Общепринято использовать специальную нотацию для последовательно идущих символов, учитывая порядок их следования в алфавите; так что продукцию для Letter, добавив еще буквы в верхнем регистре, можно записать в виде:
Аналогично можно определить Decimal_digit как '0'.. '9'. Регулярная грамматика может иметь те же виды продукций, что и
А В, то любой образец категории состоит из образца А, за которым следует без всяких разделителей образец В, никаких символов не должно быть между ними. Если языку требуется понятие разделителя, то его следует ввести явно в регулярную грамматику как лексическую категорию;А означает ранее введенную категорию, то А* и А+ означают "ноль или более повторений А" и "один или более повторений А" соответственно. Опять-таки никаких разделителей или пробелов между образцами А не предполагается;В отличие от
Выражения, допускаемые только что определенными правилами, называются регулярными выражениями, а язык, определяемый регулярной грамматикой, – регулярным языком. Отметим следующее свойство.
Доказательство следует из запрета рекурсивных определений. Как обсуждалось выше, любой появляющийся в правой части
Например:
$$A\;\triangleq\;T1\;|\;T2\;|\;T3^*\\B\;\triangleq\;T4^+\;|\;A\\C\;\triangleq\;A\;B$$Применяя процесс, описанный при доказательстве теоремы, можно построить эквивалентную грамматику, порождающую тот же язык.
$$A\;\triangleq\;T1\;|\;T2\;|\;T3^*\qquad\text{ — Без изменений}\\B\;\triangleq\;T4^+\;|\;T1\;|\;T2\;|\;T3^*\quad\text{ — Получено заменой A}\\C\;\triangleq\;(T1\;|\;T2\;|\;T3*) (T4+\;|\;T1\;|\;T2\;|\;T3*)$$Возможно, грамматика не стала более понятной, но свойство исключения нетерминалов в ней выполняется. Аналогично доказывается более сильное утверждение: любой С, рассматриваемой как начальный символ грамматики.
Теорема высвечивает принципиальное ограничение
Зато регулярные грамматики удобны для задания правил описания лексем – первичных элементов языка. Когда нужно описать лексему, состоящую из одного или нескольких символов одного вида, за которыми следует один из трех специальных символов, за которым возможно следует последовательность символов еще одного вида, для таких ситуаций регулярная грамматика – то, что требуется.
За кулисами регулярных выражений стоит математическая теория конечных автоматов. Для первого знакомства с этой теорией, о которой можно многое сказать, удобно воспользоваться визуальной иллюстрацией конечного автомата. Конечный автомат – это граф, с узлами, представляющими состояния автомата, и дугами, помеченными элементами некоторого базисного конечного множества. В нашем примере элементы представляют
Следующий пример задает синтаксическую структуру квалифицированного вызова метода в Eiffel с возможными аргументами, подобно вызову Line8.extend(new_station):
(рис 2.4) Конечный автомат, распознающий вызов метода
Конечный автомат можно рассматривать как машину, обрабатывающую x9.f_g(a,a), наш автомат стартует в состоянии 1, входной символ x станет причиной перехода в состояние 2, затем 9 станет причиной перехода в то же самое состояние 2. Символ "точка" переведет автомат в состояние 3, f переведет в 4, подчеркивание и g оставят в 4.
Появление круглой открывающей скобки переведет автомат в состояние 5, из которого автомат, обрабатывая список аргументов, будет переходить в состояние 6 и снова возвращаться в 5. Появление закрывающей скобки переведет автомат в заключительное состояние 7, у которого нет выходящих дуг.
Язык, распознаваемый конечным автоматом, – это множество всех строк, на которых автомат, начиная работать в начальном состоянии, переходит в конечное состояние, полностью прочитав строку. Строки Line8.extend (new_station) и x9.f_g(a, a) принимаются нашим автоматом и принадлежат языку, им распознаваемому. Строки не принадлежат языку автомата, если:
a.b.c (допустимая в Eiffel, но не допускаемая рассматриваемым автоматом); обработав начальную часть строки a.b, автомат перейдет в состояние 4 и остановится, поскольку в этом состоянии нет дуги, помеченной точкой – x.f(a),a;a, приводящая в состояние 2, которое не является заключительным.Основная теорема, связывающая регулярные языки и конечные автоматы, утверждает, что любой язык, заданный регулярной грамматикой, распознается конечным автоматом. Верно и обратное утверждение, что доказывает эквивалентность
Вызовы методов, распознаваемые этой грамматикой, являются подмножеством возможных в Eiffel вызовов, где выражения для аргументов допускают, подобно операторам, вложенность, как в вызове x.f(y.h(z.i)). Вышеприведенная лексическая грамматика и связанный с ней конечный автомат не распознают такие вызовы, поскольку аргумент для них может быть только идентификатором. Как только мы выходим за пределы лексем, так сразу требуется вся мощь Argument_list, позволяя определить эту категорию одной продукцией, в правой части которой стоит {Identifier "," …}+.
Конечные автоматы обеспечивают основу создания лексических анализаторов, часть компилятора, ответственную за распознавание лексем. Фактически, не представляет особого труда определить конечный автомат по регулярной грамматике, а затем по этому определению построить непосредственно программу, распознающую лексемы. Такая схема используется в лексических анализаторах.
Теория формальных языков выделяет несколько уровней по степени сложности их синтаксиса:
В качестве примера, показывающего, что контекстно-свободная грамматика не может выразить всех свойств, требуемых в большинстве языков программирования, рассмотрим правило задания типа. В x, в выражениях, таких как some_function (x), или в операторах, таких как x.some_procedure, в охватывающем модуле – методе или классе присутствовало объявление сущности в форме:
x: SOME_TYPE
Это объявление говорит, что x является локальной переменной метода или его аргументом или полем класса, а SOME_TYPE, заданному при объявлении аргумента. В противном случае программа неверна, и компилятор должен отвергнуть ее. Но это нарушение другого рода в сравнении с ошибками синтаксиса, такими как:
if c then a + b end
Здесь нарушаются правила if следовал оператор, а не выражение (как в примере).
Можно привести массу примеров, когда образцы, удовлетворяющие a: = b, где тип b не соответствует типу a.
Контекстно-свободные грамматики и А цепочкой μ, состоящей из терминалов и нетерминалов. Правило контекстно-зависимой грамматики позволяет заменять А цепочкой μ только в определенном контексте, который можно задать в виде цепочки α, определяющей левый контекст, и цепочки β, задающей правый контекст. Правило такой грамматики говорит, что цепочку αAβ с нетерминалом А можно заменить цепочкой αμβ
На практике нет формализма для контекстно-зависимых грамматик, сравнимых по простоте и практичности с
Языки классифицируются как регулярные (Тип 3), контекстно-свободные (Тип 2), контекстно-зависимые с неукорачивающими правилами (Тип 1) и неограниченные (Тип 0, для распознавания которых необходима Машина Тьюринга, другими словами, вся мощь языка программирования). Эта классификация пришла из статей, опубликованных в 1956 и 1959 годах профессором MIT Ноами Чомски (Noam Chomsky, по-русски часто произносится как Хомский) и Марком Шютценбергером (Marco Shutzennberger) из университета
(рис 2.5) Ноам Хомский (2005) |
(рис 2.6) Рави Сети (2008) |
(рис 2.7) Моника С. Лам (2008) |
|
На русском языке: Альфред В. Ахо, Моника С. Лам, Рави Сети, Джеффри Д. Ульман Компиляторы: принципы, технологии и инструментарий, 2 издание, Издательский дом "Вильямс".
Последнее издание известного учебника по методам компиляции, остающегося стандартом в течение нескольких десятилетий.
Хорошее описание современной технологии построения компиляторов.
Еще одно описание важных методов построения компиляторов.
| BNF | Choice production | Продукция "Выбор" | |
| Concatenation production | Продукция "Конкатенация" | Defining production | Определение продукции |
| Grammar | Грамматика | Lexical construct | Лексическая категория |
| Lexical grammar | Лексическая грамматика | Metalanguage | |
| Recursive grammar | Рекурсивная грамматика | Repetition production | Продукция "Повторение" |
| Phrase | Предложение | Production | Продукция |
| Top construct | Вершинная категория (начальный символ грамматики) | Vocabulary | Словарь |
Дайте точные определения терминам словаря.
Добавьте новые термины в ранее построенную карту концепций для предыдущих лекций.
Напишите Identifier, Integer, Integer_constant.
Рассмотрите язык, определяемый рекурсивной грамматикой с вершинным символом Game.
Game1 в нерекурсивной грамматике или, другими словами, любая последовательность из одного или более heads или tails, заканчивающаяся единственным символом stop, является образцом Game.Game является образцом Game1? Дайте ответ и обоснуйте его.Выпишите единственное регулярное выражение, которое полностью описывает язык, генерируемый категорией Game.
Для обоснования таких структур нам потребуется определение их синтаксиса. Первое представление структур управления использовало для их описания естественный язык, как например: "Условный оператор начинается ключевым словом if, за которым следует…". Такой стиль полезен для первого знакомства, но не позволяет задать общий способ спецификации – он многословен и недостаточно точен. Нам нужно нечто обратное – сжатость определения и математическая строгость.
Таким требованиям отвечает
Мы видели, что полное описание языка программирования включает три уровня: лексический, синтаксический, семантический.
История языков программирования началась в пятидесятые годы прошлого столетия. Первым языком, получившим широкое распространение, стал язык Фортран (FORTRAN – FORmula TRANslator), предназначавшийся для научных вычислений и спроектированный командой из фирмы IBM под руководством Джона Бэкуса в 1954 году. В 1956 году для него был разработан компилятор, что предопределило успех и послужило толчком к созданию множества языков программирования.
Вскоре американские и европейские группы объединили усилия для проектирования международного стандарта языка, ставшего известным в 1956 году под именем
При подготовке спецификаций обнаружилась необходимость лучшего способа описания синтаксиса, чем тот неформальный подход, который использовал Джон Бэкус при описании Фортрана. К этому времени Джон Бэкус входил в состав рабочей группы, создававшей язык
С тех пор было предложено много различных вариаций
(рис 2.1) |
(рис 2.2) |
Для наших целей язык – это множество "предложений", каждое из которых задается конечной последовательностью лексем из некоторого "словаря". Например, простейшим правильным предложением языка Eiffel является текст класса:
class A end
Это предложение состоит из трех лексем: двух ключевых слов и идентификатора. Тексты, встречающиеся на практике, – тексты полезных классов – имеют значительно больше лексем.
Не каждая последовательность лексем из словаря языка является предложением этого языка: переставив лексемы end A class, мы не получим текст, задающий описание класса. Синтаксис языка и определяет, какие последовательности лексем являются предложениями языка, а какие нет. Спецификация синтаксиса называется грамматикой.
Грамматикой языка называется конечное множество правил, позволяющих создавать на основе словаря языка последовательности лексем, такие, что:
Из определения следует, что любое предложение языка может быть выведено путем применения правил (утверждение 2) и что любой такой вывод дает предложение языка (утверждение 1).
Большинство языков потенциально бесконечно. Например, число возможных программ на Eiffel бесконечно. Эта теоретическая возможность не создает никаких практических проблем, во-первых, потому, что в нашей жизни мы можем иметь дело только с конечным множеством программ, но, что более важно, каждый текст класса – предложение языка Eiffel – является конечной последовательностью терминалов. Последовательность может быть очень длинной, но она не может быть бесконечной.
Конечное множество правил должно позволять порождать бесконечный язык, должно позволять, например, создавать описание всех возможных классов на языке Eiffel. Это опять-таки не должно нас беспокоить. Нам не нужны все возможные классы, нам нужны только те, что интересуют нас. Достаточно, что мы знаем, что правила способны породить описание каждого возможного класса.
Для описания грамматик будем использовать форму
class, if …) и специальные символы (точка, запятая …).Class, представляющее текст класса, и категория Conditional, представляющая текст условного оператора. Соглашение, принятое в Conditional.Conditional определяет форму любого условного оператора: вначале идет if, затем образец Каждая продукция определяет синтаксис образца категории через ограничители и другие категории. Вот пример продукции для категории Conditional:
Это правило говорит, что любой образец Conditional – любой условный оператор – состоит из ключевого слова if, ограничителя, за которым следует образец категории Then_part_list, за которым, возможно, следует образец категории Else_part, ключевое слово end завершает конструкцию. Квадратные скобки отмечают возможные конструкции, которых может и не быть. Категории Then_part_list и Else_part имеют собственные продукции.
Каждая продукция определяет одну категорию, стоящую слева от символа $$\triangleq$$, который читается как "по определению является". В правой части этого определения стоит
Idenifier (идентификатор) и Integer (целое), чьи образцы являются идентификаторами, такими как имя класса Preview, и целочисленные константы, такие как 34. Грамматика не определяет терминальные категории, их синтаксис определяется на более низком лексическом уровне.Причина отнесения некоторых категорий к терминалам и их определения вне грамматики – чисто прагматическая: эти категории имеют простую структуру, для которой мощь
На синтаксическом уровне (Identifier и Integer, имеют бесконечное множество возможных образцов.
Для любого языка особое значение имеет категория, описывающая структуру самого верхнего уровня; для Eiffel – это категория Class. Такой
Продукция, приведенная для категории Conditional, показывает, что
Conditional такими символами являются символ $$\triangleq$$ и квадратные скобки, сигнализирующие о возможности присутствия конструкции, заключенной в скобки.Conditional является синтаксической структурой, которая содержит подструктуру, такую как Then_part_list.Во избежание недоразумений нужно быть внимательным и отличать символы языка от символов
В цветном оригинале книги используются разные цвета для символов разного вида. В черно-белом варианте контекст позволяет понять, какому языку принадлежит символ. Символов
Термин "образец" (spacimen) на первых порах мог вызывать недоумение. Казалось бы, следовало использовать привычный термин "экземпляр" (instance), но этот термин уже занят – он описывает объекты. Экземпляр класса – это объект, появляющийся во время выполнения, образец класса – это текст, задающий текст некоторого конкретного класса.
Продукция определяет синтаксис одной категории. Она имеет следующую форму:
$$Construct\;\triangleq\;Definition$$В левой части продукции задается определяемая категория, а в правой – ее определение, выраженное в терминах категорий (терминалов и нетерминалов) и ограничителей. В зависимости от формы определения различают три вида продукций: конкатенацию, выбор и повторение.
Конкатенация – это продукция, перечисляющая последовательность из нуля и более категорий; они следуют в определенном порядке и некоторые из них, возможно, заключены в квадратные скобки, что и определяет их возможность отсутствия. Наша первая продукция для Conditional является таким примером:
Такая продукция задает, что каждый образец категории, стоящей слева, по определению состоит из последовательности (конкатенации) стоящих справа образцов, идущих в заданном порядке, с тем исключением, что "возможные" образцы могут отсутствовать.
Продукция "Выбор" перечисляет одну или несколько категорий, разделенных Instruction (оператор):
Продукция "Выбор" задает, что каждый образец категории, стоящей слева, по определению состоит в точности из одного образца, стоящего справа. В отличие от конкатенации, порядок следования категорий, разделенных вертикальной чертой, не имеет значения. В последнем примере продукция говорит о том, что оператор языка может быть условным оператором или оператором цикла и так далее, перечисляя все возможные виды операторов языка. При чтении продукции вертикальная черта воспринимается и произносится как "или".
Продукция "Повторение" перечисляет две категории, стоящие справа от символа определения: одну нетерминальную, которую следует повторить, другую – обычно терминальную, используемую как разделитель. Для примера определим категорию (составной оператор), которая задает последовательность операторов, разделенных символом точка с запятой:
Из этого определения следует, что образец составного оператора является последовательностью из нуля или более образцов операторов, каждый отделяется от следующего, если тот есть, символом "точка с запятой". В соответствии с этим правилом возможные образцы имеют вид:
inst1inst1; inst2inst1; inst2; inst3Здесь inst1, inst2, inst3 являются образцами операторов.
В продукции использованы новые
Такое продукционное правило для составного оператора с возможностью нулевого повторения означает, что допускается пустой составной оператор. Это может быть полезно в некоторых случаях, например, можно корректно писать:
if some_condition then [S1]
else
instruction_1
instruction_2
end
Здесь пустая then-часть законна, поскольку синтаксически здесь стоит составной оператор, пустой в этом конкретном случае. Этот пример можно переписать в более понятной форме:
if not some_condition then [S2]
instruction_1
instruction_2
end
Но первая форма может быть предпочтительнее, учитывая процесс обновления программы, когда then–часть, предположительно, будет заполнена позже.
При определении некоторых категорий однократное присутствие образца обязательно, но возможно его повторение. В этом случае вместо метасимвола * (ноль или более) используется другой Conditional, включая определения входящих в нее категорий:
Продукция "Повторение", используемая в определении категории Then_part_list, показывает, что для образца категории допустимы такие формы:
cond1 then inst1
— Один образец Then_part
cond1 then inst1 elseif cond2 then inst2
— Два образца Then_part
cond1 then inst1 elseif cond2 then inst2 elseif cond3 then inst3
— Три образца Then_part
Пример можно продолжать, поскольку число образцов в этой конструкции может быть произвольно большим. Заметьте, что категория Then_part_list, задающая список, не может быть определена как возможно отсутствующая. В определении категории Conditional после if должна следовать хотя бы одна Then_part.
В
Случай 3 предполагает, что
интерпретируются как одна продукция "Выбор":
$$A\;\triangleq\;Def1\;|\;Def2$$Варианты
Вышеприведенный пример определения категории А должен быть записан в
Вместе с несколькими нотационными соглашениями это правило стиля задает специфику
При написании определений языков я обнаружил, что это правило ведет к введению дополнительных понятий – нетерминалов, таких как Concat и Repet в последнем примере, а ранее Then_part_list, что в конечном итоге позволяет выработать более понятное описание языка.
Все, что нужно знать о
Второе применение не столь фантастично, как кажется с первого взгляда. Возможно, вам еще не скоро придется проектировать язык общецелевого применения – соперник таких языков, как С#, Java или Eiffel. Но программистам довольно часто приходится иметь дело с "малыми" языками. Всякий раз, когда приходится обрабатывать данные сложного формата, эти данные можно рассматривать как предложения некоторого языка, синтаксис которого удобно задать, используя
Третье приложение (построение анализатора) полезно при написании компиляторов и других инструментов, предназначенных для обработки программ, а в более общем случае – структурированных текстов. Одна из первых задач для таких инструментов – это реконструкция структуры текста в форме абстрактного синтаксического дерева. Это работа анализатора, как мы увидим в следующей лекции. Любому анализатору необходимо формальное описание синтаксиса языка – он может получить его из
Вторая точка зрения на практике менее полезна, но в то же время важна. Давайте исследуем ее немного глубже. Для порождения всех возможных образцов любого
| Р1 | Для конкатенации – породить все возможные последовательности образцов перечисленных категорий, учитывая, что категории со статусом "возможные" могут как присутствовать, так и отсутствовать. |
| Р2 | Для повторения – породить все последовательности из нуля и более образцов (одну или более для знака +) указанной категории с заданным разделителем элементов последовательности. |
| Р3 | Если на любом из предыдущих шагов встретился |
| Р4 | Для выбора применяйте предыдущие шаги ко всем перечисленным категориям и собирайте все образцы, порожденные каждой из категорий. |
Эти четыре механизма порождения предложений применяйте до тех пор, пока хоть одно из них будет применимо. В конечном счете будут порождены все предложения языка. Процесс этот обычно не завершается, поскольку, как мы видели, языки, представляющие интерес на практике, являются бесконечными.
Применяя этот процесс к нетерминалу А, чья продукция использует В, возможно, придется применять те же правила – на шагах Р3 и Р4 – к другим категориям.
Последнее наблюдение может вызывать некоторое опасение. Что, если, применяя процесс к А, мы должны будем применить его к В, а это приведет к тому, что мы снова встретим А? Продукция для составного оператора является хорошим примером:
Определение включает категорию Instruction, продукция для которой включает категорию $$Compound :\;\triangleq$$
Заметьте, что и определение категории Conditional включает категорию . Если попытаться понять структуру , разыскивая его образцы путем применения вышеприведенных правил, то кажется, что мы впадем в цикл – вывод, не имеющий смысла.
Определение понятия, в котором явно или неявно понятие определяется через само себя, называется рекурсивным. Рекурсия – использование рекурсивных определений – самая популярная вещь во всех областях программирования, и мы посвятим ей отдельную лекцию. Но уже здесь, где нет практически полезных грамматик, не использующих рекурсию, мы убедимся в важности рекурсивных грамматик.
Начнем изучение с небольшого примера. Рассмотрим мини-язык с тремя ключевыми словами heads, tails, stop (головы, хвосты и остановка). Других терминалов в этом языке не будет. Начальным нетерминалом будет категория Game, и вся грамматика задается тремя продукциями – выбором и двумя конкатенациями:
Ситуация напоминает ситуацию с категориями Conditional, Instruction, , определяемыми друг через друга.
Game, принадлежащие языку, который порожден этой грамматикой? Что является образцами категорий Head_start и Tail_start? Постарайтесь дать ответ прежде, чем продолжите чтение.Из-за рекурсии грамматика может показаться бессмысленной. Но будем Game одна из ветвей, stop, является терминальной, что позволяет сгенерировать первое предложение (образец Game):
stopНо теперь мы можем использовать полученную информацию для генерирования образцов Head_start и Tail_start, определяемых через Game. Соответствующие продукции скажут нам, что heads stop является образцом Head_start, а tails stop – образцом Tail_start. Воспользуемся этими образцами в продукции для Game, получим два новых образца:
heads stoptails stopПолучив эти образцы, и применяя тот же процесс, можно получить следующее множество образцов:
heads heads stopheads tails stoptails heads stoptails tails stopЭтот процесс удвоения образцов можно продолжать. Становится понятным и общая конструкция образца Game: он представляет последовательность "голов" и "хвостов", идущих в произвольном порядке, она заканчивается терминалом stop. Нетрудно доказать, что любая такая последовательность является образцом. Немного сложнее доказательство того, что образцов другого вида для Game нет.
Очень простой язык этой грамматики с начальным нетерминалом Game можно рассматривать как все последовательности возможных исходов бросания монеты при игре в "орел или решка". Последовательность заканчивается в тот момент, когда игрок кричит stop. Этот язык можно описать и не рекурсивной грамматикой:
Грамматика задана тремя продукциями, первая из которых является конкатенацией, вторая – повторением с пустым разделителем, третья – выбором.
Применяя для генерирования языка продукционные правила, мы использовали стратегию, в которой терминалы предпочтительнее нетерминалов. Выбрав другую стратегию, можно впасть в бесконечный цикл, не сгенерировав ни единого предложения. Например, начав с первой возможности для Game, получим Head_start, а после его замены – heads Game. Многократно применяя эту же стратегию для Game, получим последовательность, в которой всегда присутствует Game, что не дает создать предложение языка, которое по определению состоит только из
Во избежание подобных ситуаций процессу генерирования языка необходимы подходящие стратегии, или эвристики, подобные той, которая применялась для выбора – если есть ветвь, содержащая только терминалы, то выбирается такая ветвь, в противном случае выбирается продукция, начинающаяся с лексемы.
Даже при наличии таких эвристик процесс генерации не создаст ни одного предложения языка, если его грамматика полностью рекурсивна. Для запуска процесса генерации необходимо, чтобы по меньшей мере некоторые выборы включали только лексемы. Грамматики, такие как
$$A\;\triangleq\;A$$или
$$A\;\triangleq\;B\\B\;\triangleq\;A$$бесполезны. Эти проблемы подробно будут обсуждаться в лекции, посвященной рекурсии. Более тонким случаем являются грамматики, содержащие лексемы, но являющиеся леворекурсивными, как в следующем примере:
$$Instruction\;\triangleq\;Compound\;|\;Assignment\\Compound\;\triangleq\;Instruction\;";"\;Instruction$$Для простоты в этой грамматике Assignment считается терминалом, определенным вне грамматики (по аналогии с лексемами, определяемыми на уровне
assignment_1assignment_1; assignment_2и так далее. Для получения конструктивного вида таких рекурсивных определений необходима общая теория, набросок которой будет дан позднее.
if, то соответствующий оператор может быть только условным (Conditional); если первая лексема – from, то оператор цикла и так далее.Синтаксис, который мы изучали в этой лекции, задавал конкретный синтаксис программы со всеми ее ключевыми словами, ограничителями и прочими деталями, которые играют важную синтаксическую роль, позволяя избежать двусмысленностей, но не несут никакой семантики. Ранее мы встречались с абстрактным синтаксисом, в котором эти детали отсутствовали, оставляя только те элементы, что несут за собой смысл.
Мы видели, как описать результирующую синтаксическую структуру, используя АСД (Абстрактное Синтаксическое Дерево), такое как ранее показанное дерево, задающее синтаксис нашего класса Preview1.
(рис 2.3) Абстрактное синтаксическое дерево
Как отмечалось, проще построить конкретное синтаксическое дерево, содержащее все символы исходного текста. Некоторые компиляторы так и поступают, но обычно в этом нет необходимости. Для последующих фаз компиляции, таких как семантический анализ, генерация кода и его оптимизация, синтаксические маркеры не играют роли. Все, что нужно для представления структуры программы, в точности содержится в АСД.
Если бы нашей целью было описание , точку с запятой можно было бы опустить, оставив просто Instruction .
В таких приложениях, как синтаксический анализ и компиляция исходных текстов, эта грамматика не принесла бы пользы, поскольку, очевидно, здесь требуется конкретная грамматика, обсуждаемая до сих пор. Но она может играть свою роль, помогая в изучении тех структурных свойств текстов, которые не зависят от деталей внешнего вида этих текстов.
Одно из приложений
Детальное рассмотрение процесса построения синтаксически управляемого компилятора или просто анализатора выходит за пределы данного курса. При желании можно познакомиться с идеями применяемых методов, изучая библиотеку EiffelParse. В этой библиотеке реализован не самый эффективный механизм разбора, но ее методы представляют понятную и практическую иллюстрацию применения ОО-принципов этой книги для построения анализатора и компилятора. Сам Eiffel использует более традиционные подходы разбора, с которыми можно ознакомиться, изучая библиотеку "GOBO".
Идея, стоящая за EiffelParse, состоит в том, чтобы строить нужные классы непосредственно по грамматике , CHOICE, (соответственно для продукций "Конкатенация", "Выбор" и "Повторение"). Например, для конкатенации класс будет просто перечислять различные компоненты, стоящие в правой части продукции, связывая каждую компоненту с классом, подобным образом описывающим конструкцию. Следует быть внимательным, имея дело с
Для разбора входного текста достаточно вызвать EiffelParse – процедуру parse для соответствующей категории. В результате для нее будет создано АСД. Затем можно добавить семантическую обработку любого типа, используя методы синтаксического класса. Этот подход демонстрирует мощь и элегантность ОО-моделирования процесса анализа и компиляции языка программирования.
Для терминальных конструкций, таких как идентификаторы и числа,
На синтаксическом уровне, покрываемом
Такие категории нетрудно выразить через
Для определения структуры лексических категорий, таких как в вышеприведенных примерах, мы можем использовать регулярную грамматику – упрощенную версию
Нетерминалами такой грамматики являются категории, подобные идентификаторам и целым, которые выступают в роли терминалов в
Каждая категория выражается как выбор между единичными символами, показанными в одинарных кавычках. Такие категории являются по-настоящему терминальными (атомарными), не подлежащими дальнейшим уточнениям. Общепринято использовать специальную нотацию для последовательно идущих символов, учитывая порядок их следования в алфавите; так что продукцию для Letter, добавив еще буквы в верхнем регистре, можно записать в виде:
Аналогично можно определить Decimal_digit как '0'.. '9'. Регулярная грамматика может иметь те же виды продукций, что и
А В, то любой образец категории состоит из образца А, за которым следует без всяких разделителей образец В, никаких символов не должно быть между ними. Если языку требуется понятие разделителя, то его следует ввести явно в регулярную грамматику как лексическую категорию;А означает ранее введенную категорию, то А* и А+ означают "ноль или более повторений А" и "один или более повторений А" соответственно. Опять-таки никаких разделителей или пробелов между образцами А не предполагается;В отличие от
Выражения, допускаемые только что определенными правилами, называются регулярными выражениями, а язык, определяемый регулярной грамматикой, – регулярным языком. Отметим следующее свойство.
Доказательство следует из запрета рекурсивных определений. Как обсуждалось выше, любой появляющийся в правой части
Например:
$$A\;\triangleq\;T1\;|\;T2\;|\;T3^*\\B\;\triangleq\;T4^+\;|\;A\\C\;\triangleq\;A\;B$$Применяя процесс, описанный при доказательстве теоремы, можно построить эквивалентную грамматику, порождающую тот же язык.
$$A\;\triangleq\;T1\;|\;T2\;|\;T3^*\qquad\text{ — Без изменений}\\B\;\triangleq\;T4^+\;|\;T1\;|\;T2\;|\;T3^*\quad\text{ — Получено заменой A}\\C\;\triangleq\;(T1\;|\;T2\;|\;T3*) (T4+\;|\;T1\;|\;T2\;|\;T3*)$$Возможно, грамматика не стала более понятной, но свойство исключения нетерминалов в ней выполняется. Аналогично доказывается более сильное утверждение: любой С, рассматриваемой как начальный символ грамматики.
Теорема высвечивает принципиальное ограничение
Зато регулярные грамматики удобны для задания правил описания лексем – первичных элементов языка. Когда нужно описать лексему, состоящую из одного или нескольких символов одного вида, за которыми следует один из трех специальных символов, за которым возможно следует последовательность символов еще одного вида, для таких ситуаций регулярная грамматика – то, что требуется.
За кулисами регулярных выражений стоит математическая теория конечных автоматов. Для первого знакомства с этой теорией, о которой можно многое сказать, удобно воспользоваться визуальной иллюстрацией конечного автомата. Конечный автомат – это граф, с узлами, представляющими состояния автомата, и дугами, помеченными элементами некоторого базисного конечного множества. В нашем примере элементы представляют
Следующий пример задает синтаксическую структуру квалифицированного вызова метода в Eiffel с возможными аргументами, подобно вызову Line8.extend(new_station):
(рис 2.4) Конечный автомат, распознающий вызов метода
Конечный автомат можно рассматривать как машину, обрабатывающую x9.f_g(a,a), наш автомат стартует в состоянии 1, входной символ x станет причиной перехода в состояние 2, затем 9 станет причиной перехода в то же самое состояние 2. Символ "точка" переведет автомат в состояние 3, f переведет в 4, подчеркивание и g оставят в 4.
Появление круглой открывающей скобки переведет автомат в состояние 5, из которого автомат, обрабатывая список аргументов, будет переходить в состояние 6 и снова возвращаться в 5. Появление закрывающей скобки переведет автомат в заключительное состояние 7, у которого нет выходящих дуг.
Язык, распознаваемый конечным автоматом, – это множество всех строк, на которых автомат, начиная работать в начальном состоянии, переходит в конечное состояние, полностью прочитав строку. Строки Line8.extend (new_station) и x9.f_g(a, a) принимаются нашим автоматом и принадлежат языку, им распознаваемому. Строки не принадлежат языку автомата, если:
a.b.c (допустимая в Eiffel, но не допускаемая рассматриваемым автоматом); обработав начальную часть строки a.b, автомат перейдет в состояние 4 и остановится, поскольку в этом состоянии нет дуги, помеченной точкой – x.f(a),a;a, приводящая в состояние 2, которое не является заключительным.Основная теорема, связывающая регулярные языки и конечные автоматы, утверждает, что любой язык, заданный регулярной грамматикой, распознается конечным автоматом. Верно и обратное утверждение, что доказывает эквивалентность
Вызовы методов, распознаваемые этой грамматикой, являются подмножеством возможных в Eiffel вызовов, где выражения для аргументов допускают, подобно операторам, вложенность, как в вызове x.f(y.h(z.i)). Вышеприведенная лексическая грамматика и связанный с ней конечный автомат не распознают такие вызовы, поскольку аргумент для них может быть только идентификатором. Как только мы выходим за пределы лексем, так сразу требуется вся мощь Argument_list, позволяя определить эту категорию одной продукцией, в правой части которой стоит {Identifier "," …}+.
Конечные автоматы обеспечивают основу создания лексических анализаторов, часть компилятора, ответственную за распознавание лексем. Фактически, не представляет особого труда определить конечный автомат по регулярной грамматике, а затем по этому определению построить непосредственно программу, распознающую лексемы. Такая схема используется в лексических анализаторах.
Теория формальных языков выделяет несколько уровней по степени сложности их синтаксиса:
В качестве примера, показывающего, что контекстно-свободная грамматика не может выразить всех свойств, требуемых в большинстве языков программирования, рассмотрим правило задания типа. В x, в выражениях, таких как some_function (x), или в операторах, таких как x.some_procedure, в охватывающем модуле – методе или классе присутствовало объявление сущности в форме:
x: SOME_TYPE
Это объявление говорит, что x является локальной переменной метода или его аргументом или полем класса, а SOME_TYPE, заданному при объявлении аргумента. В противном случае программа неверна, и компилятор должен отвергнуть ее. Но это нарушение другого рода в сравнении с ошибками синтаксиса, такими как:
if c then a + b end
Здесь нарушаются правила if следовал оператор, а не выражение (как в примере).
Можно привести массу примеров, когда образцы, удовлетворяющие a: = b, где тип b не соответствует типу a.
Контекстно-свободные грамматики и А цепочкой μ, состоящей из терминалов и нетерминалов. Правило контекстно-зависимой грамматики позволяет заменять А цепочкой μ только в определенном контексте, который можно задать в виде цепочки α, определяющей левый контекст, и цепочки β, задающей правый контекст. Правило такой грамматики говорит, что цепочку αAβ с нетерминалом А можно заменить цепочкой αμβ
На практике нет формализма для контекстно-зависимых грамматик, сравнимых по простоте и практичности с
Языки классифицируются как регулярные (Тип 3), контекстно-свободные (Тип 2), контекстно-зависимые с неукорачивающими правилами (Тип 1) и неограниченные (Тип 0, для распознавания которых необходима Машина Тьюринга, другими словами, вся мощь языка программирования). Эта классификация пришла из статей, опубликованных в 1956 и 1959 годах профессором MIT Ноами Чомски (Noam Chomsky, по-русски часто произносится как Хомский) и Марком Шютценбергером (Marco Shutzennberger) из университета
(рис 2.5) Ноам Хомский (2005) |
(рис 2.6) Рави Сети (2008) |
(рис 2.7) Моника С. Лам (2008) |
|
На русском языке: Альфред В. Ахо, Моника С. Лам, Рави Сети, Джеффри Д. Ульман Компиляторы: принципы, технологии и инструментарий, 2 издание, Издательский дом "Вильямс".
Последнее издание известного учебника по методам компиляции, остающегося стандартом в течение нескольких десятилетий.
Хорошее описание современной технологии построения компиляторов.
Еще одно описание важных методов построения компиляторов.
| BNF | Choice production | Продукция "Выбор" | |
| Concatenation production | Продукция "Конкатенация" | Defining production | Определение продукции |
| Grammar | Грамматика | Lexical construct | Лексическая категория |
| Lexical grammar | Лексическая грамматика | Metalanguage | |
| Recursive grammar | Рекурсивная грамматика | Repetition production | Продукция "Повторение" |
| Phrase | Предложение | Production | Продукция |
| Top construct | Вершинная категория (начальный символ грамматики) | Vocabulary | Словарь |
Дайте точные определения терминам словаря.
Добавьте новые термины в ранее построенную карту концепций для предыдущих лекций.
Напишите Identifier, Integer, Integer_constant.
Рассмотрите язык, определяемый рекурсивной грамматикой с вершинным символом Game.
Game1 в нерекурсивной грамматике или, другими словами, любая последовательность из одного или более heads или tails, заканчивающаяся единственным символом stop, является образцом Game.Game является образцом Game1? Дайте ответ и обоснуйте его.Выпишите единственное регулярное выражение, которое полностью описывает язык, генерируемый категорией Game.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.