Uploaded by Артём Цвайгердт

otvety Vrotnev

advertisement
1) Эволюция языков программирования
Первые ЭВМ появились в 1940-х годах и программировались
исключительно на машинных языках путём последовательности нулей и
единиц, которые явно указывали компьютеру, какие операции, в каком
порядке
должны
быть
выполнены.
Сами
операции
были
очень
низкоуровневыми - переместить данные из одной ячейки памяти в другую,
сложить содержимое двух регистров, сравнить два значение и так далее.
Переход к языкам высокого уровня
Первым
шагом
создания
более
дружественных
языков
программирования была разработка мнемонических ассемблерных языков
в начале 1950-х годов. Изначально команды ассемблера являлись всего
лишь мнемоническими представлениями машинных команд. Позже в
языки ассемблера были введены макросы, так что программист мог
определять параметризированные сокращение для часто использующихся
последовательности машинных команд.
Большим шагом к высоко уровневым языкам программирования
стала
разработка
во
второй
половине
1950-х
годов
языка
программирования Fortran для научных вычислений, Cobol для обработки
бизнес данных и Lisp для символических вычислений.
В настоящее время существуют тысячи языков программирования
их можно классифицировать различными способами.
Один из способов классификация по поколениям:
1 языки первого поколения - это машинные языки
2 языки второго поколения - языки ассемблера
3 к языкам третьего поколения относятся высокоуровневые языки
программирования, такие как Fortran, Cobol, Lisp, C, C++,C# и Java.
4 языки четвёртого поколения — это языки программирования,
разработанные для конкретных применений, например, NOMAD для
генерации отчётов, SQL для запросов к базам данных, Postscript для
формирования текстов.
5
термин
языки
пятого
поколения
применяется
к
языкам
программирования, основанным на логике или ограничениях, такие как
ML
и
Haskell,
а
также
логические
языки
типа
Prolog,
часто
рассматриваются как декларативные.
2)
Термин
программирования,
язык
фон
Неймана
вычислительная
модель
применим
к
языкам
которых
основана
на
вычислительной архитектуре фон Неймана. Многие современные языки
программирования, такие как Fortran и C является языками фон Неймана.
3)
Объектно-ориентированные
программирования,
языки
поддерживающие
—
это
языки
объектно-ориентированное
программирование - стиль программирования в котором программа
состоит из набора объектов взаимодействующих друг с другом. Главными
наиболее
ранними
объектно-ориентированными
языками
программирования стали Simula 67 и Smalltalk. Примерами более поздних
объектно-ориентированных языков программирования являются С++, C#,
Java, Ruby
4) Реализация высокоуровневых языков программирования
Высокоуровневый
язык
программную
абстракцию:
использованием
языка,
а
программирования
программист
компилятор
выражает
должен
определяет
алгоритм
с
транслировать
эту
программу в целевой язык. Вообще говоря, высокоуровневый язык
программирования проще для программирования, но целевые программы
работают более медленно по сравнению с низкоуровневыми языками.
Оптимизирующие компиляторы включают технологии для повышения
производительности генерируемого кода, тем самым компенсируя
неэффективность высокоуровневых абстракций.
Новые возможности языков программирования приводили к новым
исследованиям в области оптимизация кода.
Практически
все
распространённые
языки
программирования
включая С, Fortran и Cobol поддерживают определяемые пользователем
агрегированные типы данных, такие как массивы структуры, а также
высокоуровневые средства управления потоком выполнения, такие как
циклы и вызовы процедур.
Объектная ориентированность впервые появилась в языке Simula в
1967 году и вошла в такие языки как, Smalltalk, C++, C# и Java.
Ключевыми идеями, лежащими в основе объектной ориентированности,
являются:
1 абстракция данных
2 наследование свойств
Они делают программы более модульными и упрощают и
поддержку.
Объектно-ориентированное
программ
отличается
от
программ, написанных на других языках программирования тем, что они
состоят из большего количества процедур меньшего размера (которые в
объектно-ориентированном программировании называются методами).
Таким образом, оптимизация должна быть способна перешагнуть границы
процедур исходной программы и здесь оказалась особенно полезным
встраивание процедур, представляющее собой замену вызова процедуры
её телом.
Java
обладает
возможностями,
многими
которые
облегчающими
появились
ранее
программирование
в
других
языках
программирования. Объект не может использоваться вместо объекта не
связанного типа. Обращение к массивам выполняется с проверкой выхода
за пределы массива. Java не имеет указателей и не использует
соответствующую арифметику. Этот язык программирования имеет
встроенную систему сборки мусора, которая автоматически освобождает
выделенную для переменных память, которая больше не используется.
Все эти возможности упрощают программирование, но приводят к
дополнительном накладным расходам времени выполнения.
Кроме прочего язык программирования Java разработан для
поддержки переносимости и мобильного кода.
5) Трансляции программ
Хотя обычно мы рассматриваем компиляцию как трансляцию с
высокоуровневого языка программирования на машины уровень, та же
технология применима и для трансляции между различными видами
языков программирования.
Бинарная трансляция
Методы
компиляции
могут
использоваться
для
трансляции
бинарного кода для одной машины в код для другой, обеспечивая
выполнение машинной программы, изначально скомпилированной для
другого набора машинных команд.
Методы
бинарной
трансляции
использовались
различными
компьютерными компаниями для повышения доступности программного
обеспечения для их машин.
Аппаратный синтез
На высокоуровневых языках пишется не только программное
обеспечение; даже проектирование аппаратного обеспечения, в основном,
описывается с помощью специализированных высокоуровневых языков,
предназначенных
для
описание
аппаратного
обеспечения.
Инструментарий аппаратного синтеза автоматически транслирует RTLописания в логические вентили, которые отображаются в транзисторы и в
конечном счёте – в физические схемы. Такие инструменты зачастую
выполняют оптимизацию очень длительное время.
6) Компилятор (транслятор) - эта программа, которая считывает
текст программы, написанный на одном языке исходном и транслируют
(переводит) его в эквивалентный текст на другом языке - целевом. Одна из
важных ролей компилятора состоит в сообщении об ошибках в исходной
программе, обнаруженных в процессе трансляции.
Если целевая программа представляет собой программу на
машинном языке, она затем может быть вызвана пользователем для
обработки некоторых входных данных и получения некоторых выходных
данных.
Интерпретатор - представляет собой ещё один распространённый
вид языкового процессора. Вместо получения целевой программы как в
случае транслятора интерпретатор непосредственно выполняет операции,
указанные
в
исходной
программе,
над
входными
данными,
предоставляемыми пользователем.
Целевая
программа
на
машинном
языке
производимая
компилятором, обычно гораздо быстрее, чем интерпретатор получает
входные данные на основании выходных. Однако интерпретатор обычно
обладает
лучшими
способностями
к
диагностике
ошибок,
чем
компилятор, поскольку он выполняет исходную программу инструкция за
инструкцией.
Для
более
быстрой
обработки
входных
данных
некоторые
компиляторы Java, именуемые just-in-time – компиляторами, транслируют
байт-код в машинный язык перед запуском промежуточной программы
для обработки входных данных.
Модифицированная
исходная
программа
затем
передаётся
компилятору. Компилятор может выдать в качестве входных данных
программу на языке ассемблера, поскольку ассемблерный код легче
создать и проще отлаживать. Язык ассемблера затем обрабатывается
программой, которая называется ассемблер, и даёт в качестве выходных
данных перемещаемый машинный код.
Большие программы зачастую компилируются по частям, так что
перемещаемый машинный код должен быть скомпонован совместно с
другими
перемещаемыми
объектными
файлами
и
библиотечными
файлами в код, который можно будет выполнять на данной машине.
7) Структура компилятора
Компилятор помогает нам отобразить исходную программу в
семантически
эквивалентную
ей
целевую
программу.
Процесс
отображения разделяется на две части: анализ и синтез.
Анализ - разбивает исходную программу на составные части и
накладывает на них грамматическую структуру. Затем он использует эту
структуру
для
создания
промежуточного
представления
исходной
программы. Если анализ обнаруживает, что исходная программа неверно
составлена синтаксически либо дефектна семантически, он должен выдать
информативные сообщение об этом чтобы пользователь мог исправить
обнаружены ошибки.
Синтез - строит требуемую целевую программу на основе
промежуточного представления и информации с таблиц символов. Анализ
часто называют начальной стадией (front end), а синтеза заключительной
(back end), он представляет собой последовательность фаз, каждая из
которых преобразует одно из представлений исходной программы в
другое. На практике некоторое фазы могут объединяться, а межфазное
промежуточное представление может не строиться явно. Таблица
символов, в которой хранится информация обо всей исходной программе,
используется всеми фазами компилятора.
Некоторые компиляторы содержат фазу машинно-независимой
оптимизации между анализом и синтезом. Назначение этой оптимизации преобразовать промежуточное представление так, чтобы синтез мог
получить более качественную целевую программу по сравнению с той,
которая может быть получена из неоптимизированного промежуточного
представления.
8) Лексический анализ (формирование лексем из потока символов)
Первая фаза компиляции называется лексическим анализом или
сканированием.
Лексический
анализатор
читает
поток
символов,
составляющих исходную программу, и группирует эти символы в
значащие последовательности, называющиеся лексемами. Для каждой
лексемы анализатор строит выходной токен (token) вида:
<имя_токена, значение_атрибута1,…>
Он передается последующей фазе, синтаксическому анализу.
Первый компонент токена, имя_токена представляет собой абстрактный
символ, использующийся во время синтаксического анализа, а второй
компонент значение атрибута указывает на записи таблицы символов,
соответствующую, данному токену. Информация и записи в таблице
символов необходима для семантического анализа и генерации кода.
Предположим, например, что исходная программа содержит
инструкцию присвоения:
position = initial + rate * 60
Символы в этом присвоении могут быть сгруппированы в
следующие лексемы и отображены в следующие токены, передаваемые
синтаксическому анализатору.
1.
position
-
представляет
собой
лексему,
которая
может
отображаться в токен <id,1> где id - абстрактный символ, обозначающий
идентификатор, а 1 указывает номер запись в таблице символов для
position. Запись таблицы символов для некоторого идентификатора хранит
информацию о нем, такую как имя и тип.
2. Символ присвоение = представляет собой лексему, которая
отображается в токен <=>. Поскольку этот токен не требует значение
атрибута, второй компонент данного токена опущен.
3. initial представляет собой лексему, которая отображается в токен
<id,2>, где 2 указывает на номер запись в таблице символов для initial.
4. + является лексемой, отображаемой в токен <+>
5. rate - лексема отображаемая в токен <id,3>, где 3 указывает на
номер записи в таблице символов для rate
6. * - лексема, отображаемая в токен <*>
7. 60 – лексема, отображаемая в токен <60>
Пробелы,
разделяющей
лексемы,
лексическим
анализатором
отбрасываются. При этом представлении имена токенов =,+ и *
представляют собой абстрактные символы для операторов присвоения,
сложение и умножения соответственно.
9) Синтаксический анализ
Вторая фаза компилятора синтаксический анализ или разбор
(парсинг).
Анализатор
использует
первые
компоненты
токенов
полученных при лексическом анализе, для создания древовидного
промежуточного представления, которое описывает грамматическую
структуру
потока
токенов.
Типичным
представлением
является
синтаксическое дерево, в котором каждый внутренние узел представляет
операцию, а дочерние узлы - аргументы этой операции. Синтаксическое
дерево для потока токенов показано на выходе синтаксического
анализатора на рисунке (ниже).
Это дерево указывает порядок, в котором выполняется операции в
присваивании.
position = initial + rate * 60
Дерево имеет внутренний узел, помечены знаком умножения *,
левым дочерним узлом которого является <id,3>, а правым – 60 Узел
<id,3> представляет идентификатор rate. Узел, помеченные *, явно
указывает, что сначала мы должны умножить значение rate на 60 Узел
помечены плюс +, указывает, что мы должны прибавить результат
умножение к значению initial. Корень дерева с меткой равно = говорит о
том, что следует присвоить результат этого сложения в позиции памяти,
отведённой идентификатору position. Порядок операция согласуется с
обычными арифметическими правилами, которые говорят о том, что
умножение имеет более высокий приоритет, чем сложение и должно быть
выполнено до сложения.
Последующие фазы компилятора используют грамматическую
структур, которая помогает проанализировать исходную и сгенерировать
целевую программу.
10) Семантически анализ
(смысл) использует синтаксическое дерево и информацию из
таблицы символов для проверки исходной программы на семантическую
согласованность с определением языка. Он также собирает информацию о
типах и сохраняет её в синтаксическом дереве или в таблице символов для
последующего использования в процессе генерации промежуточного кода.
Важной частью семантического анализа является проверка типов,
когда компилятор проверяет, имеет ли каждый оператор, операнды
соответствующего типа.
Например, многие определения языка программирования требуют,
чтобы индекс массива был целым числом компилятор должен сообщить
об ошибке, если в качестве индекса массива используют число с
плавающей точкой. Спецификация языка может разрешать определённые
преобразования типов, именуемые привидениями типов. Например,
бинарный арифметический оператор может быть применён либо к паре
целых чисел, либо к паре чисел с плавающей точкой. Если такой оператор
применён к числу с плавающей точкой и целому числу, то компилятор
может выполнить преобразование целого числа в число с плавающей
точкой.
11) Генерация промежуточного кода
В процессе трансляции исходной программы в целевой код
компилятор может создавать одно или несколько промежуточных
представлений различного вида. Синтаксические деревья являются видом
промежуточного представления; обычно они используются в процессе
синтаксического и семантического анализа.
После
синтаксического
и
семантического
анализа
исходной
программы многие компиляторы генерируют явное низкоуровневое или
машинное промежуточное представление исходной программы, которое
можно рассматривать как программу для абстрактной вычислительной
машины. Такое промежуточное представление должно обладать двумя
важными свойствами оно должно:
1 легко генерироваться
2 и легко транслироваться в целевой машины язык.
Рассмотрим промежуточное представление, которое называется
трёхадресным
кодом
и
состоит
из
последовательности
команд,
напоминающих ассемблерные, причём в каждой команде имеется три
операнда и каждый операнд может действовать, как регистр. Выход
генератора
промежуточного
трёхадресных кодов.
t1 = inttofloat (60)
t2 = id3 * t1
t3 = id2 + t2
id1 = t3
кода
состоит
из
последовательности
Следует сделать несколько замечаний по поводу трёх адресных
команд.
Во-первых, каждая трёхадресная команда присваивания содержит
как минимум один оператор справа. Таким образом, приведённые
команды определяют порядок выполнения операций - в исходной
программе () умножение выполняется раньше сложения.
Во-вторых, компилятор должен генерировать временные имена для
хранения значении, вычисляемых, трёхадресными командами.
В-третьих, некоторые «трёхадресные команды» наподобие первой и
последней в последовательности () содержат менее трёх операторов.
12) Оптимизация кода
Фаза машинно-независимой оптимизации кода пытается улучшить
промежуточной код, чтобы затем получить более качественный целевой
код. Обычно «более качественный», «лучший» означает «более быстрый»,
но могут применяться и другие критерии сравнения, как например «более
короткий код» или «код использующий меньшее количество ресурсов».
Например, непосредственный алгоритм генерирует промежуточный код,
используя по команде для каждого оператора в синтаксическом дереве,
полученном на выходе семантического анализатора.
Простой алгоритм генерации промежуточного кода с последующим
оптимизатором кода представляет собой рациональный способ генерации
хорошего целевого кода. Оптимизатор может определить, что
преобразование 60 из целого числа в число с плавающей точкой может быть
выполнено единственный раз во время компиляции, так что операция
inttofloat может быть устранена путём замены целого числа 60 числом с
плавающей точкой 60.0. Кроме того, t3 используется только один раз - для
передачи значение в id1в более короткую последовательность
t1 = id3 * 60
id1 = id2 + t1
Имеется большой разброс количества усилий, затрачиваемых
различными компиляторами на оптимизацию на этом этапе. Так
называемые «оптимизирующие компиляторы» затрачивают на эту фазу
достаточно много времени, в то время как другие компиляторы
применяют
здесь
только
простые
методы
оптимизации,
которые
существенно повышают скорость работы целевой программы, при этом не
слишком замедляя процесс компиляция.
13) Генерация кода
Генератор
кода
получает
в
качестве
входных
данных
промежуточное представление исходной программы и отображает его в
целевой язык. Если целевой язык представляет собой машинный код, для
каждой
переменной,
используемой
программой,
выбираются
соответствующие регистры или ячейки памяти. Затем промежуточные
команды транслируются в последовательность машинных команд,
выполняющих те же действия. Ключевым моментом генерации кода
является аккуратное распределение регистров для хранения переменных.
Например, при использовании регистров и R1 и R2 промежуточный
код () может транслироваться в машинный код.
LDF R2, id3
MULT R2, R2, #60.0
LDF R1, id2
ADDF R1, R1, R2
STF id1, R1
Первый операнд каждой команды определяет приёмник. F в каждой
команде говорит о том, что команда работает с числами с плавающей
точкой. Код на рисунке () загружает содержимое адреса id3 в регистр R2,
затем умножает его на константу с плавающей точкой 60 решётка #
указывает, что 60 следует рассматривать как непосредственно значение.
Третья команда помещает id2 в регистр R1, а четвёртая прибавляет к
нему предварительно выставленное и сохранённое в регистре R2 значение.
Наконец, значение регистра R1 сохраняется по адресу id1, так что код
корректно реализует инструкцию присваивания ().
Это беглое знакомство с генерацией кода полностью игнорируют
важный вопрос о распределение памяти для идентификаторов в исходной
программе, как вы увидите позже, организация памяти во время
выполнения программы зависит от компилируемого языка. Решения о
распределении
памяти
принимаются
либо
в
процессе
генерации
промежуточного кода, либо при генерации целевого кода.
14) Инструментарий для создания компиляторов
Разработчики компиляторов, как и разработчики любого другого
программного обеспечения, могут с успехом использовать современное
средство разработки программного обеспечения, содержащий такие
инструменты, как редакторы языков, отладчики, средства контроля
версий, профайлеры, средства тестирования и тому подобное. В
дополнение к этим средством общего назначения может использоваться
ряд более специализированных инструментов, созданных для помощи в
реализации различных фаз компилятора.
Эти инструменты используют собственные специализированные
языки для описания и реализации отдельных компонентов, и многие из
них основаны на весьма сложных алгоритмах. Наиболее успешными
являются те инструменты, которые скрывают детали алгоритма генерации
и создают компоненты, легко интегрируемые в компилятор. К широко
используемым инструментам для создания компиляторов относятся
следующие:
1 Генераторы синтаксических анализаторов, которые автоматически
создают синтаксически анализаторы на основе грамматического описание
языка программирования.
2 Генераторы сканеров, которые создают лексические анализаторы
на основе описания токенов языка с использованием регулярных
выражений.
3 Средства синтаксически управляемой трансляции, которые
создают наборы подпрограмм для обхода синтаксического дерева и
генерации промежуточного кода.
4 Генераторы генераторов кода, которые создают генераторы кода
на основе наборов правил трансляции каждой операции промежуточного
языка в машинный язык для целевой машины.
5 Средства работы с потоком данных, которые облегчают сбор
информации о передаче значений от одной части программы ко всем
другим. Анализ потоков данных представляет собой ключевую честь
оптимизации кода.
6 Наборы для построения компиляторов, которые представляют
собой интегрированные множества подпрограмм для построения разных
фаз компиляторов.
15) КОНЕЧНЫЙ АВТОМАТ
В основе теории построения компиляторов лежит теория автоматов,
поэтому мы начнём с конечного автомата одного из основных понятий,
под автоматом мы конечно подразумеваем не реально существующее
устройство, а некоторую математическую модель, свойства и поведения
которой, можно изучать и которую можно имитировать с помощью
программы на реальной вычислительной машине.
Конечный автомат является простейшей из моделей теории
автоматов и служит управляющим устройством для всех остальных
изучаемых в ней автоматов. Помимо, того, что они служат основой теории
автоматов, конечные автоматы находят непосредственно применение в
ряде ситуаций возникающих при построении компиляторов, благодаря
следующим их свойствам:
1. Конечный автомат может решать, по крайней мере, в первом
приближении ряд лёгких задач компиляции в частности лексический блок
почти всегда строятся на основе конечного автомата.
2.
Поскольку
при
моделировании
конечного
автомата
на
вычислительной машине обработка одного выходного символа требует
небольшого количества операций, программа работает быстро.
3. Моделирование конечного автомата требуют фиксированного
объёма памяти, что упрощает проблемы связанные с управлением
памятью.
4.
Существует
ряд
теорем
и
алгоритмов
позволяющих
конструировать и упрощать конечный автомат, предназначенный для тех
или иных целей.
Термин «конечный автомат», в действительности употребляется в
разных смыслах, в зависимости от подразумеваемых приложений, в
литературе по теории автоматов существует несколько различных
формальных определений, общим в этих определениях является то, что
они моделируют вычислительное устройство с фиксированным и
конечным объёмом памяти которое читает последовательности входных
символов принадлежащих некоторому конечному множеству.
Принципиальное различие в определениях связаны с тем, что
автоматы
делают
на
выходе.
Следующий
раздел
начинается
с
рассмотрение конечного автомата единственным выходом, которого
является указания на то допустимо или нет данная входная цепочка
(последовательность символов). Допустимой мы называем правильно
построенную, или синтаксически правильную цепочку, например цепочка,
которая должна изображать числовую константу, построена неправильно,
если содержит две десятичной точки.
16) КОНЕЧНЫЙ РАСПОЗНАВАТЬ
Конечный распознаватель эта модель устройства с конечным числом
состояний, которая отличает правильно образованную или допустимую
цепочку от недопустимых.
Примером
задачи
распознавания
может
служить
проверка
нечетности числа единиц произвольной цепочки состоящей из нулей и
единиц. Соответствующий конечный автомат будет допускать все цепочки
содержащие нечётное число единиц и отвергать цепочки с чётным их
числом.
На вход конечного автомата подаётся цепочка символов из
конечного множество называемого входным алфавитом автомата и
представляющая собой совокупность символов для работы, с которыми он
предназначен. Входной алфавит контролера нечётности состоит из двух
символов 0 и 1.
Контролер нечётности будет построен так, чтобы он умел запомнить
чётное или нечётное число единиц встретилось ему при чтении отрезка
входной цепочки. Поэтому множество состояний нашего автомата
содержит два состояния, которое мы будем называть «ЧЁТ» и «НЕЧЕТ».
Одно из этих состоянии должно быть выбрано в качестве
начального. Начальным состоянием контролера нечётности будет «ЧЁТ»,
так как на первом шаге число прочитанных единиц равно нулю и ноль чётное число.
При чтении очередного входного символа состояние автомата
меняется, причём новое его состояние зависит только от входного символа
и текущего состояния. Работу автомата можно описать математически с
помощью функции называемой () функцией переходов.
По текущему состоянию (S тек) и текущему входному символу (х)
она даёт новое состояние автомата (S нов).
Символически эта зависимость описывается так:
(S тек,х) = S нов
Определим функцию переходов контролера нечётности следующим
образом:
(ЧЕТ,0) = ЧЕТ
(ЧЕТ,1) = НЕЧЕТ
(НЕЧЕТ,0) = НЕЧЕТ
(НЕЧЕТ,1) = ЧЕТ
Эта функция переходов отражает тот факт, что чётность меняется
тогда и только тогда, когда на входе читается единица. Некоторые
состояния
автомата
выбираются
в
качестве
допускающих
или
заключительных.
Если автомат начав работу в начальном состоянии, при прочтения
всей цепочке переходит в одно из допускающих состояний говорят, что
эта входная цепочка допускается автоматом. Если последнее состояние
автомата не является допускающим говорят, что автомат отвергает
цепочку. Контролер нечётности имеет единственное допускающее
состояние - «НЕЧЕТ».
Суммируя все сказанное можно дать следующее определение
конечного распознавателя.
Конечный автомат задаётся:
1 конечным множеством входных символов;
2 конечным множеством состоянии;
3 функцией переходов (), которая каждой паре состоящей из
входного символа и текущего состояния приписывает некоторое новое
состояние;
4 состоянием выделенным, в качестве начального;
5 подмножеством состоянии выделенных в качестве допускающих
или заключительных.
Первый символ 1 меняет состояние «ЧЁТ» на «НЕЧЕТ», так как
функция (ЧЕТ,1) = НЕЧЕТ.
Следующая единица меняет «НЕЧЕТ» на «ЧЁТ». Ноль оставляет
автомат в состоянии «ЧЁТ». Последняя единица изменяет состояние на
«НЕЧЕТ». Так как «ЧЁТ» - начальное, а «НЕЧЕТ» - допускающее
состояние, цепочка 1101 допускается нашим автоматом. Входную цепочку
101 автомат отвергает, так как она переводит его изначального состояния
в состояние не являющаяся допускающим.
Регулярным множеством - называется множество цепочек, которое
распознается некоторым конечным распознавателем.
Таким образом, множество цепочек из нулей и единиц с нечётным
числом единиц может служить примером регулярного множества.
17) ТАБЛИЦА ПЕРЕХОДОВ
Один из удобных способов представления конечных автоматов таблица переходов, для контролёра нечётности такая таблица изображена
на рисунке (2.1).
Информация размещается в таблице переходов в соответствии со
следующими соглашениями:
1 Столбцы помечены входными символами.
2 Строки помечены символами состояний.
3 Элементами таблицы являются символы новых состоянии,
соответствующих входным символам столбцов и состояниям строк.
4 Первая строка помечена символом начального состояния.
5
Строки
соответствующие
допускающим
(заключительным)
состояниям, помечены справа единицами, а строки соответствующие
отвергающим состояниям, помечены справа нулями.
Таким образом, таблица переходов, изображённая на рисунке (2.1)
задаёт конечный автомат у которого:
1 входное множество = {0,1}
2 множество состояний = {ЧЕТ, НЕЧЕТ}
3 переходы = (ЧЕТ,0) = ЧЕТ, … и т.д. см. выше.
4 начальное состояние = ЧЕТ
5 допускающая состояние = {НЕЧЕТ}.
Ещё один автомат изображён на рисунке 2.2. Входная цепочка
XYZZ допускается этим автоматом так как:
и 3 является допускающим состоянием, тогда как цепочка ZYX
отвергается потому, что
и 2 - отвергающее состояние.
18)
КОНЦЕВЫЕ
МАРКЕРЫ
И
ВЫХОДЫ
ИЗ
РАСПОЗНАВАНИЯ
Конечный распознавать, лежит в основе процессов распознавания
цепочек в компиляторе. Один из способов использования такого
распознавателя поставить его под контроль некоторой управляющей
программы,
которая
определяет
момент,
когда
входная
цепочка
прочитана, и по состоянию распознавателя выясняет, допустима она или
нет.
Рассмотрим автомат, изображённый на рисунке ниже.
Он допускает множество цепочек в алфавите {A,B} таких что
символ «B» в них либо не встречаются, либо встречаются парами,
например, этот автомат допускает цепочки: ABB, ABBA, AAA,
ABBBBABB, но отвергает: BAA, ABBB и ABBAB. Состояние 1 помнит,
что обработанная часть цепочки допустима. Если после допустимой части
цепочки следует символ «B», автомат переходит в состояние 2. В
состояние E он переходит, когда входная цепочка окончательно испорчена
вхождением символа «A» вслед за «неспаренным» «B». Таким образом,
состоянии E можно назвать «состоянием ошибки», которое запоминает,
что обнаружена ошибка.
Пусть теперь нам нужно построить программу или компилятор,
который прочитав цепочку, или программу в алфавите {A,B} вызывает
процедуру «ДА» если цепочка принадлежит множеству распознаваемому
автоматом рисунок (2.3) и процедуру «НЕТ» в случае, когда цепочка не
принадлежит этому множеству, хотелось бы конечно, чтобы компилятор
имитировал работу автомата простым и естественным способом,
поскольку именно конечный автомат лежит в основе теории построения
компиляторов.
В описанных выше ситуациях будем считать, что цепочка,
подаваемая на вход автомата, имеет концевой маркер. Пусть это будет
символ (-|).Тогда цепочка ABB поступит на вход автоматов виде:
Автомат, изображённый на рисунке (2.3) нужно изменить так, чтобы
он умел обрабатывать дополнительной символ
«-|»
Преобразованный автомат изображён на рисунке (2.4). Символ
«ДА» это сокращённое указание на то, что работа закончена и автомат
должен выйти на процедуру «ДА». Новое состояние не наступает, так как
на этом автомат свою работу заканчивает.
С введением концевого маркера необходимо заметить, что следует
различать алфавит обрабатываемого языка и входной алфавит автомата,
осуществляющего обработку. В рассматриваемом примере, алфавит языка
по-прежнему {A,B} и концевой маркер в описании языка не участвует.
Входным алфавитом автомата, распознающего этот язык (рисунок
первый) также остаётся {A,B}, тогда как входным алфавитом автомата
обрабатывающего тот же язык (рисунок второй) будет {A,B,-|}.
Метод, с помощью которого мы получили обрабатывающий автомат
из распознающего очень прост. Мы добавили столбец, помеченный
концевым маркером, и поместили «ДА» в строки соответствующие
допускающим
состояниям
и
«НЕТ»
в
строки
соответствующие
отвергающим состояниям. Ясно, что этим же способом каждый
распознавать может быть преобразован в обрабатывающий автомат.
Поэтому
очевидно,
распознавателей
что
любая
потенциально
техника
применима
построения
и
при
конечных
построении
обрабатывающих автоматов (процессоров).
Рассмотрим, например, задачу распознавания цепочек из нулей и
единиц содержащих хотя бы одну пару стоящих рядом единиц.
Соответствующий
распознаватель
переходит
в
состояние
С
при
обнаружении пары 11
Если мы хотим получить распознавать, который выходит на «ДА»
как только обнаружена пара единиц, то переходы в состоянии С нужно
заменить на «ДА». Распознаватель превратится, таким образом, в
процессор. В нем нет состояния С, так как переходы в него отсутствует.
19) Пример построения автомата
Чтобы продемонстрировать автоматную технику на конкретном
примере, построим автомат для распознавания цепочек, которые могут
следовать за словом INTEGER в операторах спецификации Фортрана.
Примеры таких операторов:
INTEGER А
INTEGER X, I(3)
INTEGER C(3, J, 4), B
Будем считать, что массивы могут иметь любую размерность, хотя в
большинстве стандартов и компиляторов Фортрана она ограничена.
Входной алфавит будет состоять из пяти символов: U C, (), где U Лексема,
означающая
произвольную
переменную,
C
-
Лексема,
соответствующая целочисленной константе
Построение автомата осуществляется, используя эвристический
приём, который называется «разметкой символов». На первом шаге
построения из определяемого множества надо выбрать одну или более
типичных цепочек и пометить входящие в них символы.
Если на интуитивном уровне ясно, что множество цепочек, которые
могут следовать за некоторым символом совпадает с множеством цепочек
следующих за другим символом то этим символам приписываются
одинаковые метки. Эти метки отражают одно эвристическое понятие,
которое называется «ролью».
Номер 1 зарезервирован для начального состояния автомата и
помещён перед началом цепочки.
Заметим, что две запятые помечены номером 5 Действительно,
цепочка, 4 ) , В, которая следует за второй запятой, могла встретиться и
после первой запятой, образуя допустимую цепочку
После этих запятых могут следовать одни и те же цепочки, т. е. две
запятые выполняют одну и ту же «роль». Так, если цепочку, идущую за
этой запятой поместить после запятой, играющий роль 5, то мы получим
не допустимую цепочку.
Аналогичным образом оба целых числа помечены номером 4,
поскольку они допускают одинаковые продолжения.
Роли, встречающиеся в нашем примере, словами могут быть
описаны так:
2 - имя переменной, описываемой как INTEGER;
3 - левая скобка;
4 - целое число, задающее размерность;
5 - запятая, разделяющая размерности;
6 - переменная, задающая переменную размерность;
7 - правая скобка;
8 - запятая, разделяющая объекты, описываемые как целые.
В нашем примере на первом шаге вводятся начальное состояние 1 и
состояние ошибки Е.
На втором шаге мы вводим состояния 2-8, соответствующие ролям,
выявленным при интуитивном анализе.
Затем на шаге 3 получаем все нужные переходы. Заметим, что
первым символом каждой допустимой цепочки, должна быть переменная в
роли 2, поэтому заносят в таблицу переход от начального состояния 1 в
состояние 2 при чтении входного символа V.
Этот элемент мы видим в таблице, изображённое на рисунке (2.7),
где показан конечный результат нашего построения. За вхождением
символа с ролью 2 может следовать либо вхождения с ролью 3, как в
цепочке «INTEGER А (» либо с ролью 8 как в цепочке «INTEGER А,».
Поэтому для соответствующих входных символов помещаем в таблицу
переходы из 2 в 3 и из 2 в 8 Завершая этот анализ, во все остальные ячейки
таблицы заносим переходы в состояние Е.
Символы, стоящие в конце допустимых цепочек, могут иметь роль 2
или 7, поэтому объявляем состояние 2 и 7 допускающим и на это
завершаем построения таблицы.
20) ПУСТАЯ ЦЕПОЧКА
Можно сказать, например, что цепочка:
1. состоит из следующих друг за другом символов,
2. образуется сцеплением символов.
Однако все это в сущности сводятся к определению: «цепочка - это
последовательность символов».
Во-первых,
рассмотрим
пустую
цепочку
как
программу
вычислительной машины. Если заключить её в управляющие операторы и
ввести в вычислительную машину для компиляции, то листинг будет
выглядеть так:
BEGIN
END
Если мы хотим, чтобы пустую цепочку обработал один из автоматов
необходимо добавить к ней справа концевой маркер, т. е. на вход автомата
поступит цепочка. -|
Автомат допустит эту цепочку, если элемент таблицы переходов,
соответствующий начальному состоянию и концевому маркера, содержит
«ДА», и отвергнет её если этот элемент содержит «НЕТ». Если
рассмотреть конечный распознаватель, рассмотренный выше, то станет
ясно, что.
Конечный распознавать допускает, пустую цепочку тогда и только
тогда, когда его начальное состояние является допускающим.
В
терминах
переходов
это
означает,
что
пустая
цепочка
применённая к начальному или любому другому состоянию не вызывает
никаких переходов, то есть оставляет состояние неизменным. Таким
образом, под действием пустой цепочки, применённой к начальному
состоянию, распознаватель заканчивает работу в начальном состоянии,
которое и определяет допустимости цепочки.
Последовательность переходов для пустой цепочки, применённой к
произвольному состоянию S выглядит так: S.
Следовательно, если первое состояние в этой последовательности (S)
является начальным, а последнее (тоже S) - допускающим, то пустая
цепочка допускается.
Мы изображали пустую цепочку, помещая её между управляющими
операторами или снабжая концевыми маркером, но изобразить её
непосредственно невозможно, так как она присутствуют в предложении
незримо. Решим эту проблему обозначая пустую цепочку через () .
Символически () определяется следующим равенством: g==
Пустую цепочку путают иногда с пустым множеством хотят цепочка
и множество совершенно разные понятия. Пустым (или нулевым)
называют множество, не содержащее ни одного элемента; его часто
обозначают через  или , Чтобы пояснить это различие, сравним пустое
множество { } и множество {}.
Пустое множество, понятно не содержит элементов, тогда как
множество  содержит один элемент а именно пустую цепочку .
 не является входом автомата, распознающего {}. Символ 
используется в описаниях и сам по себе не является входным символом
автомата.
Другое заблуждение связано с отождествления пустой цепочки и
знака пробела. Однако выражение 10 обозначает цепочку 10 а не 1_0.
Поскольку пустая цепочка не содержит ни одного символа, её длина равна
0. Длина же цепочки состоящий из одного символа пробела равна 1.
21) ЭКВИВАЛЕНТНОСТЬ СОСТОЯНИЙ
Для каждого конечного автомата существует бесконечное число
других конечных автоматов, которые распознают тоже множество
цепочек.
При
конструировании
распознавателя
для
данной
проблемы
естественно учитывать возможность существования какого-то другого
распознавателя, для того - же множества, который определяется проще, и
при реализации в качестве программы для вычислительной машины
требует меньших затрат памяти.
Один из результатов теории касающегося этого вопроса заключается
в том, что для каждой задачи распознавания существует единственный
автомат
свойства
которого
полностью
соответствуют
нашим
представлениям об автомате, «имеющем простейшее определение» и
«требующим минимальных затрат памяти при реализации».
В частности, устанавливается следующий факт:
Для каждого конечного распознавателя существует единственный
конечный автомат, распознающий то же самое множество цепочек, при
этом число его состояний не больше числа состояний любого другого
конечного распознавателя для этого множества.
Для любого автомата можно получить новый автомат с таким же
числом состояний, просто переименовав его состояния. Однако имена
состоянии не имеют никакого значения для распознавания цепочек, или
для реализации автомата как программы вычислительной машины.
Поэтому на практике автоматы, которые различаются лишь именами
состояний можно считать «одинаковыми».
Этот единственный автомат - «минимальный» автомат. При
изложении теории станет очевидным, что «минимальный» автомат для
заданной проблемы распознавания является на самом деле результатам
приведения более громоздких автоматов, решающих ту же задачу. Суть в
том, что «минимальной» автомат - это компактный вариант автоматов
большего объёма, а не просто ещё один автомат у которого случайно
оказалась меньше состояний. Этот факт усиливает довод в пользу выбора
минимального автомата в качестве главного кандидата на реализацию.
Первым шагом в изложение теории минимизации и привидения
автоматов будет введение понятие «эквивалентности» состояний.
Неформально два состояния эквивалентны, если они одинаково
реагирует на все возможные продолжения входной цепочки.
Это понятие применимо к состояниям одного и того же автомата и к
состояниям
разных
распознавателям,
автоматов.
назначение
Применительно
которых
-
к
конечным
допускать
цепочки,
эквивалентность состояний можно определить так:
Состояние s конечного распознавателя М эквивалентно состоянию t
конечного распознавателя N тогда и только тогда, когда автомат M, начав
работу в состоянии s, будет допускать в точности те же цепочки, что и
автомат N начавший работу в состоянии t.
Если два состояние s и t одного автомата эквивалентны, то автомат
можно упростить, заменив в таблице переходов все вхождения имён этих
состоянии каким-нибудь новым именем, а затем удалив одну из двух
строк, соответствующих s или t.
Например, состояние 4 и 5 автомата изображённого на рисунке
ниже, явно имеют одинаковые функции, так как оба они являются
допускающими,
оба переходят в состояние 2 при чтении входного символа А и оба
переходят в состояние 3 при чтении В. Поэтому мы объединяем состояние
4 и 5 в одно состояние, для которого выбираем имя Х. Заменяя в таблице
состояний каждое вхождение имён 4 и 5 имеем Х, мы получаем тем самым
таблицу, изображённую на рисунке (б). Две её строки помечены Х; удалив
одну из них, получаем упрощённую таблицу состояний на рисунке (в).
Вторая цель проверки состояний на эквивалентности состоит в
выяснении того, делают ли два автомата одно и тоже, то есть совпадают
ли множество допускаемых ими цепочек. Это достигается просто
проверкой эквивалентности начальных состоянии автоматов.
Если они эквивалентны, то по определению эквивалентности
состояний оба автомата допускают и отвергают одни и те же цепочки.
Таким образом, понятие эквивалентности состояний приводит нас к
понятию эквивалентности автоматов, а именно: Автоматы M и N
эквивалентны тогда и только тогда, когда эквивалентны их начальные
состояния.
Если два состояния не эквивалентны, то любая цепочка, под
действием которой одно из них переходит в допускающее состояние, а
другое в отвергающее состояние называется цепочкой различающей эти
два состояния.
Например, состояние А и Х на рисунке ниже(прямоугольники) не
эквивалентны так как их различает цепочка 101 Символически это
выглядит так:
Причём состояние С - допускающее, а состояние Z - нет поскольку
A и X - начальные состояния соответствующих автоматов.
Два состояния эквивалентны тогда и только тогда, когда не
существует различающей их цепочки.
Заметим,
что
понятие
эквивалентности
состоянии
является
отношением эквивалентности в математическом смысле это отношение:
1 рефлексивно (каждое состояние эквивалентно себе);
2 симметрично (из того, что S эквивалентно T следует, что T
эквивалентно S);
3 транзитивно (если S эквивалентно T, а T эквивалентно U то S
эквивалентно U).
22) ПРОВЕРКА ЭКВИВАЛЕНТНОСТИ ДВУХ СОСТОЯНИЙ
Метод
проверки эквивалентности состояний, основанные на
следующем факте:
Состояния s и t эквивалентны тогда и только тогда, когда
выполняются следующие два условия:
1 условие подобия - состояние s и t должны быть либо оба
допускающим, либо оба отвергающими.
2 условие преемственности - для всех входных символов состояние s
и t должны переходить в эквивалентные состояния т. е. их преемники
эквивалентны.
Оба условия выполняются тогда и только тогда, когда s и t не имеет
различающей цепочки.
Если нарушено хотя бы одно из них, то существует цепочка,
различающая эти два состояния.
Если не выполняется условия подобия, то различающей цепочкой
является пустая цепочка. Если нарушено условие преемственности, то
некоторый
входной
символ
«x»
переводит
состояние
s
и
t
в
неэквивалентные состояния. Поэтому «x» с приписанной к нему цепочкой,
различающей эти новые состояния, образует цепочку, различающую s и t.
Если состояния s и t различаются некоторое цепочкой, то хотя бы
одно из этих условий должно быть нарушено.
Если их различает пустая цепочка (нулевой длины), то не
выполняется условие подобия. Если длина различающей цепочки больше
нуля, то её первый символ переводит s и t в пару состояний, которые не
эквивалентны, так как
различаются
оставшейся
частью цепочки,
различающей s и t.
Значит,
оба
условия
выполняются,
если
два
состояния
эквивалентны, и что хотя бы одно из них нарушается в случае
неэквивалентности состояний. Условия 1 и 2 можно использовать в общем
методе проверки на эквивалентность произвольной пары состояний. Этот
метод, вероятно лучше понимать, как проверку на неэквивалентность и
рассматривать его как метод поиска различающей цепочки.
Для записи необходимых данных мы будем строить таблицы нового
типа, которые назовём таблицами эквивалентности состояний.
Сначала мы проверяем на эквивалентность состояния 0 и 7 рисунок.
Таблица эквивалентности состоянии для этой проверки содержит по
одному столбцу для каждого входного символа, а именно столбец для y и
столбец для z.
Строки будут добавляться в ходе проверки. Первоначально имеется
одна строка которая помечена парой состояний подвергаемых проверке, а
именно парой 0,7. Результат изображён на рисунке ниже (а).
Сначала рассматриваем неэквивалентность состояний 0 и 7, показав,
что нарушается условие подобия.
К сожалению, это условие выполняется, так как оба состояния
являются отвергающими. Теперь смотрим на то, что будет нарушено
условие преемственности.
Чтобы исследовать эту возможность, рассмотрим, как действует на
данную пару состоянии, каждый входной символ, и запишем результат в
соответствующую ячейку таблицы. Так как состояние 0 и 7 под действием
входного, символа y переходят в состояние 0 и 6 соответственно, мы
записываем 0,6 в столбец таблицы, соответствующей символу y.
Так как оба состояние 0 и 7 переводятся символом z в состояние 3,
запишем 3 в столбец для z. Теперь мы получили рисунок (б). Чтобы
нарушалось условие преемственности, должны быть не эквивалентными
либо состояния 0 и 6 либо состояние 3 и 3 Так как каждое состояние
эквивалентно самому себе, состояния 3 и 3 автоматически эквивалентны.
Чтобы исследовать на неэквивалентность состояния 0 и 6, добавляем
к таблице эквивалентности состояний новую строку и помечаем её этой
парой. Результат показан на рисунке (в). Процесс теперь повторяется с
этой новой строкой.
Сначала мы проверяем условие подобия для состояний 0 и 6
Обнаруживаем, что 0 и 6 неэквивалентны, так как шесть допускающее, а
ноль отвергающее состояние. Проверка закончена, значит, что исходное
состояние 0 и 7 неэквивалентны.
Строка 0,6 появилась как результат применения входного символа
«y» к паре «0,7», поэтому «y» является различающей цепочкой.
Теперь проверим, эквивалентны ли состояние 0 и 1 начнём
построения таблицы эквивалентности состояний с пары 0,1. Эти состояния
подобны, поэтому мы вычисляем результат применения к ним каждого
входного символа и помещаем полученные состояния в таблицу.
Получаем рисунок ниже (а). Наши надежды на неэквивалентность
состояний 0 и 1 оправдаются, если будет установлена неэквивалентность
состояний 0, 2 или 3, 5. Поэтому мы добавляем в таблицу строку для
каждой из этих пар. Результат изображен на рисунке ниже (б).
Обратившись к строке 0,2, мы замечаем, что эти состояния подобны
и поэтому надо вычислить следующую пару состояний, чтобы проверить
условие преемственности. Результат показано на рисунке выше (в).
Из двух новых элементов таблицы лишь один даёт новую строку, а
именно пара 3,7 другой элемент пара 0,2 уже имеется в таблицы, и нет
надобности его проверять.
Тот факт, что пара 0, 2, порождается алгоритмом дважды, означает,
что имеются две входные цепочки, которые ведут из исходной пары 0,1 в
пару состоянии 0,2. Любая из этих цепочек с приписанной к ней цепочкой,
различающей состояние 0 и 2, образует различающую цепочку для
состояний 0 и 1.
Однако поскольку для доказательства неэквивалентности состоянии
0 и 1 достаточно одной различающей их цепочки, достаточно одного
вхождения в таблицу пары 0,2. Следовательно, единственной новой
строкой в таблице будет строка 3,7.
Затем устанавливаем, что состояния 3 и 5 подобны. Вычисляем
следующие пары, а именно 6,6 и 5,7. Так как состояние 6 эквивалентно
самому себе, единственный новой строкой будет 5,7, в этот момент наша
таблица выглядит, так как изображено на рисунке выше (г). Продолжая
процедуру, мы не обнаруживаем ни одной пары неподобных состоянии и
ни одной новой пары, которую надо проверять наподобие.
Таблица заполнена, как показано на рисунке выше (д), и поиск
различающей цепочки окончился неудачей. Поэтому состояние 0 и 1
должны быть эквивалентным.
Общую процедуру можно описать следующим образом:
1 Начать построения таблицы эквивалентности состояний с
отведения столбца для каждого входного символа.
2 Выбрать в таблице эквивалентности состояний строку, ячейки
которые ещё не заполнены, и проверить, подобны ли состояния, которыми
она помечена. Если они не подобны, то два исходных состояния
неэквивалентны и процедура оканчивается. Если они подобны, вычислить
результат применения каждого входного символа к этой паре состояний и
записать полученные пары состояний в соответствующие ячейки
рассматриваемой строки.
3 Для каждого элемента таблицы, полученного на шаге 2,
существует три возможности:
a. Если элементом таблицы является пара одинаковых состояний, то
для этой пары не требуется никаких действий.
b. Если элементом таблицы является пара состояний, которые уже
использовались как метки строк, то для неё также не требуется никаких
действий.
c. Если элемент таблицы - это пара разных состояний, которые ещё
не использовалась как метка, то для этой пары состоянии нужно добавить
новую строку. В данном случае порядок состояний в паре не важен, и
пары s, t и t, s считаются одинаковыми.
4 Если все строки таблица эквивалентности состоянии заполнены,
исходная пара состоянии и все пары состояний, порождённые в ходе
проверки, эквивалентны и проверка закончена. Если таблица не заполнена,
нужно обработать ещё по крайней мере одну её строку, применяется шаг 2
Так как каждая пара, появившаяся в заполненной таблице
эквивалентности состояний, содержит эквивалентные состояния, этот
метод проверки часто даёт больше информации, чем предполагалось
вначале.
Информацию об эквивалентности состояний можно использовать
для упрощения автомата. Мы объединяем состояния 0, 1 и 2 в одно
состояние которое называем А, а состояния 3, 5 и 7- в состояние, которое
называем В. Удаляя лишние строки, мы получаем более простой
эквивалентный автомат, изображённое на рисунке ниже.
23) НЕДОСТИЖИМЫЕ СОСТОЯНИЯ
Среди состояний автомата могут быть такие, которые не достижимы
из начального состояние ни для какой входной цепочки. На рисунке ниже
(а) таким состоянием является «s4», так как в таблице нет перехода в «s4».
Такие состояния, как «s4», называются недостижимым.
Строки, соответствующие этим состояниям, можно удалить из
таблицы переходов получив тем самым таблицу переходов автомата,
которая эквивалентна исходной, но имеет меньшее число состояний. Это
сделано на рисунке выше (б).
Для любого заданного автомата довольно просто составить список
достижимых состояний.
1. Начать список начальным состоянием.
2. Для каждого состояния, уже внесённого в список, добавить все
ещё не занесённые в него состояния, которые могут быть достигнуты из
этого нового состояние под действием одного входного символа.
Если эта процедура перестаёт давать новые состояния, то все
достижимые состояния получены, а все остальные состояние можно
удалить из автомата. Так как на каждом шаге процедуры к списку
достижимых состояний добавляется хотя бы одно новое состояние, число
шагов процедуры ограничено числом состояний данного автомата.
В качестве примера рассмотрим автомат на рисунке выше (а). Начав
с состояния «s0», мы видим, что состояния «s1» и «s5» наступают под
действием одного входного символа. Из состояния «s1» есть переход в
«s2» и «s7»; из состояния «s5» - в «s3» и «s1». Таким образом, нам
известно что «s0», «s1», «s5», «s2», «s7» и «s3» достижимы, и нужно
посмотреть, есть ли переходы в какие-нибудь новые состояния из «s2»,
«s7» и «s3».
Проверка этих состояний показывает, что никакие новые состояния
не достигаются, и, следовательно, оставшиеся состояния «s4», «s6» и «s8»
недостижимы. Таким образом, эти три состояния можно удалить, получив
тем самым эквивалентный автомат, изображённый на рисунке выше (в).
24) НЕДЕТЕРМИНИРОВАННЫЕ АВТОМАТЫ
Недетерминированный автомат - это просто формализм для
определения множеств цепочек. Слово «автомат» присутствует в его
названии потому, что этот формализм является обобщением формализма,
используемого
для
определения
обычного
(детерминированного)
автомата.
Недетерминированные конечные распознаватели важны, поскольку:
для
1.
заданного
множества
иногда
легче
найти
недетерминированное описание;
2.
существует
недетерминированного
процедура
для
конечного
превращения
распознавателя
произвольного
в
обычный
(детерминированный) конечный распознаватель.
Недетерминированный
конечный
распознаватель
представляет
собой обычный распознаватель с той разницей, что значениями его
функций переходов являются множества состояний, а не отдельные
состояния, и вместо одного начального состояния задаётся множество
начальных
состояний.
Таким
образом,
можно
дать
следующее
определение:
Недетерминированный конечный распознаватель задаётся:
1 конечным множеством входных символов,
2 конечным множеством состоянии,
3 функцией переходов δ (дельта), которая каждой паре, состоящей
из состояния и входного символа, ставит в соответствие множество новых
состояний,
4 подмножеством состояний, выделенных в качестве начальных,
5 подмножеством состоянии, выделенных в качестве допускающих.
Если состояние Sнов. принадлежит множеству новых состояний,
приписанному функцией переходов «текущему» состоянию Sтек. и
входному символу «х», то мы пишем:
Этим обозначением можно, конечно, пользоваться и в том случае,
когда мы предпочитаем не интерпретировать его как переход некоторого
реального автомата. Говорят, что автомат допускает входную цепочку,
если она позволяет связать одно из его начальных состояний с одним из
допускающих. Так, если для некоторого автомата справедливо:
где S0 - начальное состояние, а S3 - допускающее состояние, то мы
будем говорить, что входная цепочка «х1х2х3» допускается этим
автоматом.
Входная цепочка длины n допускается недетерминированным
конечным распознавателем тогда и только тогда, когда можно найти
последовательность состояний «S0 … Sn», такую, что S0 - начальное
состояние, Sn - допускающее состояние, и для всех «i», таких, что (0 < i <=
n), состояние Si принадлежит множеству новых состояний, приписанных
функцией переходов состоянию Si-1 для «i-го» элемента входной цепочки.
Способ представления конечных распознавателей с помощью
таблицы
переходов
легко
распространяется
и
на
представление
недетерминированных конечных распознавателей. Необходимо сделать
лишь два изменения.
Во-первых, каждый элемент таблицы должен содержать множество
состояний. Мы указываем это множество, просто перечисляя его элементы
и не заключая их в скобки.
Второе изменение состоит в том, что начальные состояния
указываются с помощью стрелок, расположенных перед метками
соответствующих строк. Если таких стрелок нет, подразумевается, что
есть
только
одно
начальное
состояние,
а
именно
состояние
соответствующая первой строке.
На рисунке (2.20) изображена таблица переходов, представляющая
недетерминированный конечный распознаватель.
Множество состояний – {А, В, С}, входное множество – {0, 1},
допускающие состояния – {В, С} и начальные состояния - {А,В}.
Переходы такие:
Причём
B
-
начальное
состояние,
а
С
–
допускающее.
Существование одной этой последовательности переходов достаточно для
того,
чтобы
показать
допустимость
входной
цепочки
«11»,
и
существование другой последовательности переходов из начального
состояния, в отвергающее, например
на это не влияет.
Один из переходов недетерминированного распознавателя, а именно
δ(С,0) является переходом в пустое множество.
Это попросту означает, что для состояния С и входа 0 дальнейшие
переходы невозможны. Такой элемент таблицы переходов может
препятствовать
существованию
некоторой входной цепочки.
последовательности
переходов
для
В данном примере такой цепочкой является «10»; так как 1
переводит оба начальных состояния в состояние С, множество преемников
которого пусто. Такие входные цепочки просто отвергаются наряду со
всеми прочими цепочками, которые не могут перевести начальное
состояние в заключительное.
«Работу» недетерминированного автомата можно интерпретировать
двояким образом. Покажем это на примере автомата, приведённого на
рисунке выше. Пусть автомат находится в состоянии А, и к нему
применяется входная цепочка, начинающаяся с 0.
Тогда можно представить себе один из следующих вариантов:
1 Автомат осуществляет выбор, переходя либо в А либо в В, т. е. в
одно из новых состояний, соответствующих старому состоянию А и входу
0 Автомат продолжает работать подобным образом, и при этом возможно
много выборов. Если имеется какая-нибудь последовательность выборов,
при которой автомат под действием входной цепочки заканчивает работу
в допускающем состоянии то говорят, что это входная цепочка
допускается автоматом. Достаточно только одной последовательности
выборов, приводящей к допускающему состоянию, и автомат допускает
данный вход, даже если имеется много других последовательностей
выборов, которые не ведут к допускающему состоянию.
2 Автомат распадается на два автомата, один - в состоянии А, а
другой - в состоянии В. При продолжении обработки входа происходит
дальнейшее деление каждого автомата в соответствии с возможностями,
содержащимися в таблица переходов. Когда вход обработан, цепочка
допускается, если один из результирующих автоматов находятся в
допускающем состоянии.
Эти две интерпретации эквивалентны, и обе полезны для понимания
недетерминированного
автомата.
Однако
автомат
служит
не
для
моделирования этих ситуаций. Его назначение состоит в определении
допустимого множества входных цепочек.
25) ЭКВИВАЛЕНТНОСТЬ НЕДЕТЕРМИНИРОВАННЫХ И
ДЕТЕРМИНИРОВАННЫХ КОНЕЧНЫХ РАСПОЗНАВАТЕЛЕЙ
Понятие
недетерминированного
конечного
распознавателя
приобретает практическое значение благодаря следующему факту:
Для каждого недетерминированного конечного распознавателя
существует детерминированный конечный распознаватель, который
допускает
в
точности
те
же
входные
цепочки,
что
и
недетерминированный.
Основная идея построения заключается в том, что после обработки
отдельной входной цепочки состояние детерминированного автомата
будет
представлять
собой
множество
всех
состояний
недетерминированного автомата, которые он может достичь из начальных
состояний после применения данной цепочки.
Переходы детерминированного автомата можно получить из
недетерминированных
переходов,
вычисляя
множество
состояний,
которые могут следовать после данного множества при различных
входных символах. Допустимость цепочки определяется по тому, является
ли последнее детерминированное состояние, которого он достиг,
множеством недетерминированных состоянии, включающим хотя бы одно
допускающее состояние.
Результирующий детерминированный автомат является конечным,
так
как
существует
лишь
конечное
число
подмножеств
недетерминированных состояний.
Если недетерминированный автомат имеет (n) состояний, то
эквивалентный детерминированный автомат, который мы только что
описали, может иметь 2^n состояний, по числу подмножеств исходного
множества состояний.
Процедура задаётся следующими пятью шагами. Пусть Mн –
недетерминированный
автомат,
а
Mд
-
эквивалентный
детерминированный автомат, который нужно построить.
ему
1 Пометить первую строку таблицы переходов для Mд множеством
начальных состояний автомата Мн. Применить к этому множество шаг 2
2 По данному множеству состояний S, помечающему строку
таблицы переходов автомата Мд, для которой переходы ещё не
вычислены, вычислить те состояние Мн, которые могут быть достигнуты
из S с помощью каждого входного символа «x», и поместить множества
последующих состоянии в соответствующие ячейки таблицы для Мд.
Если δ (дельта) - функция недетерминированных переходов, то функция
детерминированных переходов δ` (дельта штрих) задаётся формулой:
3 Для каждого нового множества, порождённого переходами на
шаге 2, посмотреть, имеется ли уже в Мд строка, помеченная этим
множеством. Если нет, то создать новую строку и пометить её эти
множеством. Если множество уже использовалась как метка, никаких
действий не требуется.
4 Если в таблице автомата Мд есть строка, для которой ещё не
вычислены переходы, вернуться назад и применить к этой строке шаг 2
Если все перехода вычисления перейти к шагу 5.
5 Пометить строку как допускающее состояние автомата Мд тогда и
только
тогда,
когда
она
содержит
допускающее
состояние
недетерминированного автомата.
Результат применения шага 1 изображён на рисунке ниже (а).
Применяя шаг 2 к {А, В}, обнаруживаем, что δ`({А, В},0) = {A, В} и
δ`({А, В},1) = {С}. См. рис. ниже (б).
Применяя шаг 3, мы видим, что уже имеется строка для {А, В}, но
не для {C}. Поэтому создаём новую строку для {С},получая тем самым
конфигурацию на рисунке (2.22,в).
Переходя к шагу 4, обнаруживаем, что надо применить шаг 2 к {C}.
После того как это сделано, на шаге 3 выясняется, что нужны ещё две
строки рисунок ниже (г). Применение шага 2 к {A, C} и к пустому
множеству { } даёт нам переходы в множества, которые уже являются
именами состояний. Этот результат изображён на рисунке ниже (д).
Теперь шаг 4 предписывает нам перейти к шагу 5. Состояние {A, B}
отмечается как допускающее, поскольку оно содержит допускающее
состояние B; состояния {C} и {A, C} отмечаются как допускающие, так
как содержат допускающее состояние С. Пустое множество, разумеется,
не содержит допускающего состояния и поэтому помечается как
отвергающее.
Результат
приведён
на
рисунке
ниже
(е),
где
изображён
окончательный вариант детерминированного автомата, эквивалентного
исходному недетерминированному. Чтобы напомнить, что множества на
рисунке (,е) - это просто имена состояний нового автомата, мы
подставляем новый набор имён, получая на рисунке ниже (ж), таблицу
состоянии, которая задает тот же самый автомат, но в более простых
обозначениях.
Теоретически
состояниями,
недетерминированный
изображённый
на
рисунке
автомат
с
десятью
(2.21),
может
иметь
детерминированный вариант с 1024 состояниями, в соответствии с числом
подмножеств множества, содержащего десять состояний, но на самом деле
потребовалось только девять состояний. Это меньше исходного числа.
Таким образом, мы видим, что порождение только необходимых
подмножеств - чрезвычайно полезны приём.
Глядя на таблицу переходов, изображённую на рисунке 2.23, мы
замечаем, что не надо делать различия между ролями Л1 и Л2, а также А1
и А2. Алгоритм позволяет нам не заботятся об этом при создании
исходного распознавателя.
Хотя процедура гарантирует, что детерминированный автомат не
содержит недостижимым состояний, детерминированный автомат может
оказаться не минимальным. В последнем, примере состояния {O} и {Ь}
явно эквивалентны и могут быть объединены.
26) АВТОМАТЫ С МАГАЗИННОЙ ПАМЯТЬЮ
Чтобы получить более мощный автомат, память конечного автомата
расширяется за счёт дополнительного механизма хранения информации.
Один из методов хранения информации, которой оказался весьма
полезным в компиляции и просто реализуется - это использования
магазина или стека.
Основная особенность магазинной памяти с точки зрения работы с
нею состоит в том, что символы можно помещать в магазин и удалять из
него по одному, причём удаляемый символ - это всегда тот, который был
помещён в магазин последним.
Последовательность символов в магазинной памяти можно сравнить
со стопкой тарелок в кафетерии. Служащие кафетерия ставят чистые
тарелки наверх стопки, и посетители затем берут их тоже сверху. Таким
образом, посетитель всегда берет из стопки тарелку поставленной туда
последней.
Когда информация помещается в магазин, мы говорим, что она
«ВТАЛКИВАЕТСЯ» в магазин. Когда информация удаляется из магазина,
мы говорим, что она «ВЫТАЛКИВАЕТСЯ» из него.
Говорят, что информация, только что поступившая в магазин,
находится в его верхушке или наверху. Хотя такое представление о работе
с магазином полезно, оно мало что говорит о том, как реализовать магазин
в компиляторе.
Мы изобразили магазин на рисунке выше (а). На дне магазина
находится символ «Δ» а наверху - символ С. Символы расположены в том
порядке, в каком они поступали в магазин. Сначала поступил символ «Δ»,
затем нижнее А, затем В, затем верхнее А и наконец символ С.
Если втолкнуть в магазин символ D то магазин будет выглядеть, так
как показано на рисунке (б), где D – верхний символ магазина. Если же,
наоборот, вытолкнуть из магазина верхний символ С, то верхним
символом окажется А, и магазин будет выглядеть, как показано на
рисунке (в). В обоих случаях изменением подвергается только верх
магазина, а остальные символ остаются неизменными.
Символ «Δ» - это специальный символ, который помечает начало
или «дно» магазина и называется «МАРКЕРОМ ДНА». Он используется
только как метка дна и никогда не выталкивается из магазина. Так, если
«Δ» верхний символ магазина, как на рисунке (г), то мы знаем, что других
символов в магазине нет. В этом случае говорят, что магазин пуст.
Магазин на рисунке (а) можно также изобразить в виде цепочки одним из
следующих способов:
Представление магазина в первой строке соответствует соглашению
о том, что «верхней символ находится слева», а во второй строке - что
«верхней символ справа». Которое из двух соглашений использовано,
можно определить по маркеру дна.
Одной из моделей автомата, в которых используется магазинный
принцип организации памяти, является «АВТОМАТ С МАГАЗИННОЙ
ПАМЯТЬЮ». В нем очень просто комбинируется память конечного
автомата и магазинная память. МП-автомат может находиться в одном из
конечного числа состоянии и иметь магазин, куда он может помещать и
откуда может извлекать информацию.
Как и в случае конечного автомата, обработка входной цепочки
осуществляется за ряд мелких шагов. На каждом шаге действия автомата
конфигурация его памяти может измениться за счёт перехода в новое
состояние, а также вталкивания символа в магазин или выталкивания из
него. Однако в отличие от конечного автомата. МП-автомат может
обрабатывать один входной символ в течении нескольких шагов.
На каждом шаге управляющее устройство автомата решает, пора ли
закончить обработку текущего входного символа и получить новый
входной символ или продолжить обработку текущего символа на
следующем шаге.
На рисунке сверху изображена одна из конфигураций, которая
может возникнуть при обработке некоторым гипотетическим МПавтоматом входной цепочки 100110 Для большей наглядности входная
цепочка изображена записанной в ячейках файла или ленты с указателем
на входной символ, подвергающийся в данный момент обработке.
Каждый шаг процесса обработки задаётся множеством правил,
использующих информацию трёх видов:
1 состояние,
2 верхний символ магазина,
3 текущей входной символ.
Это множество правил называется управляющим устройством или
механизмом
управления.
Всё
на
том
же
рисунке
информация,
информации,
управляющее
поступающая в управляющее устройство, такова:
-состояние 6,
-верхней символ магазина С
-текущий входной символ 0
В
зависимости
от
получаемой
устройство выбирает либо выход из процесса (т. е. прекращает обработку),
либо переход в новое состояние. Переход состоит из трёх операций:
-над магазином,
-над состоянием,
-над входом.
Возможно операции таковы:
Операций над магазином
1. Втолкнуть в магазин определённый магазинный символ.
2. Вытолкнуть верхний символ магазина.
3. Оставить магазин без изменений.
Операция над состоянием
1. Перейти в заданное новое состояние.
Операции над входом
1. Перейти к следующему входному символу и сделать его текущим
входным символом.
2. Оставить данный входной символ текущим, иначе говоря,
«ДЕРЖАТЬ» его до следующего шага.
Обработку входной цепочки МП-автомат начинает в некотором
выделенном состоянии при определённом содержимом магазина, а
текущим входным символом является первый символ входной цепочки.
Затем автомат выполняет операции, задаваемые его управляющим
устройством.
Если
происходит
выход
из
процесса,
обработка
прекращается. Если происходит переход, то он даёт новый верхний
магазинный символ, новый текущей символ, автомат переходит в новое
состояние и управляющее устройство определяет новое действие, которое
нужно произвести.
Что бы управляющие правила имели смысл, автомат не должен
требовать следующего входного символа, если текущим символом
является концевой маркер, и не должен выталкивать символ из магазина,
если это маркер дна. Поскольку маркер дна может находиться
исключительно на дне магазина, автомат не должен также вталкивать его
в магазин.
МП-автомат определяется следующими пятью объектами:
1 конечным множеством входных символов, в которое входит и
концевой маркер;
2 конечным множеством магазинных символов, включающим
маркер дна;
3 конечным множеством состояний, включающим начальное
состояние;
4 управляющим устройством, которое каждой комбинации входного
символа, магазинного символа и состояния ставит в соответствие выход
или переход. Переход в отличие от выхода заключается выполнения
операций над магазином, состоянием и входом, как было описано выше.
Операции, которые запрашивали бы входной символ после концевого
маркера или выталкивали из магазина, а также вталкивали в него маркер
дна, исключаются;
5 Начальным содержимым магазина, которое представляет собой
маркер дна, за которым следует цепочка других магазинных символов.
МП-автомат называется, МП-распознавателем, если у него два
выхода - ДОПУСТИТЬ и ОТВЕРГНУТЬ. Говорят, что цепочка символов
входного алфавита допускается распознавателем, если под действием этой
цепочки с концевыми маркером, автомат начавший работу в своём
начальном состоянии и с начальным содержимым магазина, делает ряд
переходов, приводящих к выходу ДОПУСТИТЬ. В противном случае
цепочка отвергается.
При описании переходов МП-автомата будем обозначать действия
автомата словами ВЫТОЛКНУТЬ, ВТОЛКНУТЬ,
СОСТОЯНИЕ, СДВИГ и ДЕРЖАТЬ, причем:
ВЫТОЛКНУТЬ означает вытолкнуть верхний символ магазина,
ВТОЛКНУТЬ (А), где А - магазинный символ, означает втолкнуть символ
А в магазин, СОСТОЯНИЕ (s), где s – состояние, означает, что
следующим состоянием становятся s, СДВИГ означает, что текущем
входным символом становится следующий входной символ. В некоторых
реализациях это может означать сдвиг указателя на входе.
ДЕРЖАТЬ означает, что текущей входной символ надо держать до
следующего шага, т. е. оставить его текущим.
Когда
нам
нужно
определить
переход,
который
оставляет
содержимое магазина неизменным, это выражается в том, что мы
опускаем слова ВЫТОЛКНУТЬ и ВТОЛКНУТЬ. Хотя ДЕРЖАТЬ по
существу означает, что СДВИГ отсутствует, мы всегда будем записывать
операции над входом в явном виде, чтобы было понятнее, что происходит.
Когда будет обнаружена соответствующая правая скобка, символ А
будет выталкивается из магазина. Цепочка отвергается, если на входе
остаются правые скобки, а магазин пуст или если цепочка прочитана до
конца, а в магазине остаются символы А. Цепочка допускается, если к
моменту прочтения входной цепочки до конца магазин опустошается.
Полное определение таково:
Здесь комбинации входного символа, магазинного символа и
состояние расположен слева от знака равенства, а переходы - справа от
него. 5 Начальное содержимое магазина «Δ».
Работа автомата на рисунке снизу, как он обрабатывает цепочку
«(( )( ))».
На рисунке показан каждый шаг процесса обработки, начиная с
начальной конфигурации на рисунке (а) и конечная допускающая
конфигурацией на рисунке (з). Такое изображение последовательности
конфигураций МП-автомата требуют много места, поэтому представим её
в таком более компактном виде:
В этом линейном представление конфигураций МП-автомата
магазин изображён слева, состояние - в средине, а необработанная часть
входной цепочки - справа. Эта часть входной цепочки включает текущий
входной символ и символы, которые следуют после него.
Чтобы восстановить всю входную цепочку, нужно вернуться назад,
к исходной конфигурации. Информацию, поступающую в управляющее
устройство, выделить очень легко, так как символ, расположенный
наверху магазина, находится слева от состояния, а текущий входной
символ - справа от него.
27) КОНТЕКСТНО-СВОБОДНЫЕ ГРАММАТИКИ
Такие элементы, как <подлежащее> или <существительное>,
играющие роль членов предложения или частей речи, называются
НЕТЕРМИНАЛЬНЫМИ (вспомогательными) символами или просто
нетерминалами. В контекстно-свободной грамматике может быть любое
конечное
число
программирования
нетерминалов.
не
терминалами
При
определении
служат
такие
языков
элементы,
как
<оператор>, <арифметическое выражение> и т.д.
Такие элементы как ДУБ, ЗАСЛОНЯЕТ, играющие роль слов из
словаря языка, называются ТЕРМИНАЛЬНЫМИ (основными) символами
или просто терминалами. Контекстно-свободная грамматика может
содержать
любое
конечное
число
терминалов.
В
языках
программирования терминалами являются фактически используемые в
них слова и символы, такие, как DO, IF, + и т.д.
Правила грамматики иногда называют продукциями и в общем виде
выглядят так:
ОДИН НЕТЕРМИНАЛ -> (типа стрелочка) ЛЮБАЯ КОНЕЧНАЯ
ЦЕПОЧКА ИЗ ТЕРМИНАЛОВ И НЕТЕРМИНАЛОВ.
Цепочка справа от стрелки может быть пустой. Пример такого
правила: <A> - > ε
Иногда правило с пустой правой частью мы будем называть
ЭПСИЛОН-ПРАВИЛОМ.
Контекстно-свободная
грамматика
может
содержать любое конечное множество продукций. Пример продукцией
языка программирования:<оператор> -> IF <логическое выражение>
THEN <оператор>.
Один из нетерминалов выделен как начальный нетерминал или
начальный символ, с которого должны начинаться выводы цепочек языка.
Для
естественных
языков
таким
не
терминалом
может
быть
<предложение>, для языков программирования <программа>. Начальный
символ мы будем часто обозначать через <S>.
Суммируя все сказанное, будем задавать контекстно-свободную
грамматику:
1 конечным множеством нетерминалов;
2 конечным множеством терминалов, которое не пересекается с
множеством нетерминалов;
3 конечным множеством правил вида: <A> -> α где <А> нетерминал, а α - цепочка терминалов и нетерминалов (возможно пустая);
нетерминал <A> называется левой частью правила, а α - правой часть;
4 одним нетерминальным символом, выделенным в качестве
начального.
Если множество правил приводится без специального указания
множества
нетерминалов
и
терминалов,
то
предполагается,
что
грамматика содержит в точности те нетерминалы и терминалы, которые
встречаются в правилах.
Пусть, например, даны четыре таких правила:
Если больше ничего не определено, предполагается, что множество
нетерминалов – {S, A}, так как это те терминалы, которые встречаются в
левых частях правил. Предполагается также, что множество терминалов –
{a, b, c}, так как это остальные символы, используемые в правилах.
Понятно, что символ «ε» в правиле 4 представляет пустую цепочку и не
является символом грамматики. Правило 4 можно записать без «ε» в таком
виде:
4 A->
Как было сказано в предыдущем абзаце, можно определять
грамматику, задавая её правила и начальный не терминал.
Нетерминальное и терминальное множества нужно задавать в явном
виде лишь тогда, когда они не совпадают с множествами, которые
получаются описанным способом.
Так, если бы мы хотели, чтобы терминальный множеством в
последнем примере было множество - {a, b, c, d}, то нам пришлось бы
задать его в явном виде. В этом случае автомат, распознающий язык,
воспринимал бы d как вход, хотя этот символ не входит ни в одну
допустимую цепочку.
Чтобы легче было различать нетерминальные и терминальные
символы, применим соглашение заключать нетерминалы в угловые скобки
при их определении. В соответствии с этим соглашением правила и с
последнего примера записываются как показано на рисунке ниже.
Благодаря этому достаточно посмотреть на какую-нибудь цепочку вроде
a<A>b<B>, чтобы понять, какие символы нетерминальные а какие
терминальные.
Хотя обозначения, используемые нами для описания грамматик,
довольно широко распространены в соответствующие литературе часто
используется ещё один способ записи, называемой формой БЭКУСА –
НАУРА или БНФ. В этих обозначениях -> заменяется символом ::==, за
которым может следовать любое число правых частей, разделённых
вертикальный чертой |. Здесь также нетерминалы заключается в угловые
скобки. Пользуясь БНФ, мы запишем грамматику, приведённую на
рисунке выше, так:
Идею совмещения правых частей можно применять, разумеется, и
при записи со стрелкой ->, но для ясности мы предпочитаем писать
каждое правило в отдельной строке, так, чтобы можно было нумеровать
строки, как на рисунке наерху, и затем ссылаться на них по номерам.
28) ФОРМАЛЬНЫЕ ЯЗЫКИ И ФОРМАЛЬНЫЕ ГРАММАТИКИ
Чтобы отличать употребление слова «язык» в значении точно
определённого множества цепочек от употребления этого слова в
повседневной речи, множество цепочек называют иногда «формальным
языком». Чтобы применить математический подход к проблемам,
связанным с языками и их обработкой, мы должны ограничиться
множествами цепочек, которые можно определить некоторым точным
образом. Есть много способов точного задания таких множеств. Один
способ, например, заключается в задании языка как множества,
допускаемого каким-нибудь распознавателем цепочек вроде конечного
автомата или автомата с магазинной памятью. Другой подход состоит в
использовании методов, которые можно считать грамматическими.
Термин
«формальная
грамматика»
применим
к
любому
определению формального языка, основанному на «грамматических
правилах», с помощью которых можно порождать и анализировать
цепочки аналогично тому, как грамматики используются при изучении
естественных языков. В этой лекции мы займёмся особым видом
формальных
грамматик
называемых
контекстно-свободными
грамматиками.
ФОРМАЛЬНЫЕ ГРАММАТИКИ: ПРИМЕР
Давайте рассмотрим формальную грамматику которая в какой-то
степени напоминает фрагмент грамматики русского языка и задаёт
формальный язык, состоящий из четырёх русских предложений.
В этой формальной грамматике используются элементы, играющие
роль членов предложения или частей речи.
<предложение>,
<подлежащее>,
<сказуемое>,
<дополнение>,
<прилагательное>,<существительное>.
Мы заключаем их в угловые скобки, чтобы отличать их от слов из
фактического словаря, составляющих предложения языка. В нашем
примере словарь состоит из следующих пяти слов или «символов».
ДОМ, ДУБ, ЗАСЛОНЯЕТ, СТАРЫЙ,(точка) .
В
грамматике
имеются
определённые
правила,
содержащие
информацию о том, как из этих символов можно строить
предложения языка. Одно из этих правил таково:
1 <предложение> -> <подлежащее> <сказуемое> <дополнение>.
Это правило интерпретируется следующим образом: «Предложение
может состоять из подлежащего, за которым следует сказуемое, затем
дополнение и точка». В грамматике вполне могут быть и другие правила,
задающий предложения другой структуры. Однако в данной грамматике
таких правил нет.
Остальные правила таковы:
2 <подлежащее> -> <прилагательное> <существительное>
3 <дополнение> -> <прилагательное> <существительное>
4 <сказуемое> -> ЗАСЛОНЯЕТ
5 <прилагательное> -> СТАРЫЙ
6 <существительное> -> ДОМ
7 <существительное> -> ДУБ
Применим эту грамматику для порождение предложения. По
правилу
1
предложение
имеет
вид:
<подлежащее>
<сказуемое>
<дополнение>.
Так как, согласно правилу 2, подлежащим может быть комбинация:
<прилагательное> <существительное>
Её можно подставить вместо подлежащего и получить предложение,
которое имеет вид:
<прилагательное> <существительное> <сказуемое> <дополнение>.
Аналогичным образом можно применить правило 3, чтобы заменить
<дополнение> и получить:
<прилагательное>
<существительное>
<сказуемое>
<прилагательное> <существительное>.
Теперь можно дважды применить правило 5, чтобы, заменив
<прилагательное>, получить:
СТАРЫЙ
<существительное>
<сказуемое>
СТАРЫЙ
<существительное>.
Применяя
правила
6
и
7,
заменяющие
первое
и
второе
<существительное>, и правило 4, заменяющее <сказуемое>, получаем
готовое предложение: СТАРЫЙ ДОМ ЗАСЛОНЯЕТ СТАРЫЙ ДУБ.
Этот вывод можно наглядно изобразить в виде дерева смотри
рисунок ниже. Дерево показывает, какие правила применялись к
различным
промежуточным
элементам,
но
скрывает
порядок
их
применения. Таким образом, можно видеть, что результирующая цепочка
не зависит от порядка, в котором делались замены промежуточных
элементов.
Иногда говорят, что дерево представляет собой «синтаксической
структуру» предложения. Идея вывода подсказыват другие интерпретации
правил, подобных правилу.
<подлежащее> -> <прилагательное> <существительное>
Вместо того чтобы говорить «<подлежащее> - это <прилагательное>
за
которым
следует
<существительное>»,
можно
сказать,
что
<подлежащее> «порождает» (или из него «выводится», или «его можно
заменить на») <прилагательное> <существительное>.
C помощью этой грамматики можно вывести также три других
предложение а именно:
СТАРЫЙ ДУБ ЗАСЛОНЯЕТ СТАРЫЙ ДОМ.
СТАРЫЙ ДОМ ЗАСЛОНЯЕТ СТАРЫЙ ДОМ.
СТАРЫЙ ДУБ ЗАСЛОНЯЕТ СТАРЫЙ ДУБ.
Эти три предложения и предложение, выведенное раньше, и есть все
предложения, порождаемые данной грамматикой. Множество, состоящее
из этих четырёх предложений, называется языком, который определяется
грамматикой.
29) СИНТАКСИЧЕСКИ УПРАВЛЯЕМОЙ ПРОЦЕСС ОБРАБОТКИ
ЯЗЫКОВ
В отличие от конечного распознавателя для распознавателей с
магазинной памятью (МП-распознавателя) строить соответствующие
процедуры достаточно трудно, поэтому теория распознавания контекстносвободных языков сама по себе не обеспечивает адекватной теоретической
базы для построения компилятора.
Все методы проектирования, рассматриваемые в последующих
разделах, основываются на технике, в которой процесс обработки
контекстно-свободного языка определяется в терминах обработки каждого
отдельного правила соответствующей грамматики. Для описания процесса
обработки,
основанного
на
этой
технике,
обычно
используется
прилагательное «СИНТАКСИЧЕСКИ УПРАВЛЯЕМЫЙ».
Синтаксически
основываются
на
управляемые
методы
математическом
понятии
в
данном
контексте
«ТРАНСЛИРУЮЩЕЙ
ГРАММАТИКИ», которое вводится далее.
30) ПОЛЬСКАЯ ЗАПИСЬ
Обычный метод записи арифметических выражений, известен под
названием «ИНФИКСНОЙ ЗАПИСИ». Однако существуют другие
способы описание того, как нужно комбинировать арифметические
величины.
Одним
из
таких
способов
является
так
называемая
«ПОСТФИКСНАЯ ПОЛЬСКАЯ ЗАПИСЬ» разработанная польским
математиком Я. Лукасевичем.
Рис. 7.1
В постфиксной польской записи знак операции следует сразу за её
операндами. Множество «польских выражений» с операциями + и *
можно породить при помощи грамматики
где «I» обозначает любую переменную. В дальнейших рассуждениях
эти переменные представляются малыми латинскими буквами. Каждому
выражению в инфиксной записи соответствует выражение в постфиксной
польской записи.
Например, постфиксной записью для a*b будет ab*, а для a*b+c
будет ab*c+ а на рисунке (7.1) показано дерево выводапоследней цепочки.
Выражение показывает, что к операнду ab* (т. е. произведению а и
b) и операнду c надо применить операцию +.
Для
другого
примера
инфиксного
выражения
a+b*c
соответствующей постфиксной польской записью будет abc*+. Дерево
вывода этой цепочки показано на рисунке (7.2). Выражение состоит из
операнда «a», операнда «bc*» и знака операции +.
Постфиксная польская запись не содержит скобок, даже когда
соответствующие инфиксные выражения должны заключаться в скобки.
Например, инфиксное выражение (a+b)*c записывается как ab+c*. В
качестве последнего примера возьмём выражение a+b*(c+d)*(e+f),
постфиксной польской записью для которого будет abcd+*ef+*+.
Постфиксная польская запись обеспечивает другой язык для записи
математических формул. Некоторые компиляторы имеют синтаксический
блок,
который
буквально
переводит
инфиксные
выражения
в
соответствующие польские записи.
31) ТРАНСЛИРУЮЩИЕ ГРАММАТИКИ
Если нужно построить процессор, получающий в качестве входа
инфиксное выражение и печатающий на выходе эквивалентное выражение
в постфиксной польской записи. Чтобы проектирование этого процессора
основывалось на распознавателе, которой каждый раз, когда должен быть
выдан символ, вызывает процедуру печати.
На входе - цепочка a+b*c. Действия процессора, соответствующие
вводу и выводу, могли бы происходить по такому «сценарию»:
1 ЧИТАТЬ(a)
6 ЧИТАТЬ(*)
2 ПЕЧАТАТЬ(a)
7 ЧИТАТЬ(с)
3 ЧИТАТЬ(+)
8 ПЕЧАТАТЬ(с)
4 ЧИТАТЬ(b)
9 ПЕЧАТАТЬ(*)
5 ПЕЧАТАТЬ(b)
10 ПЕЧАТАТЬ(+)
Этот сценарий правдоподобен, поскольку символы, a, b и c
печатаются, как только они поступают на вход, а операторы печатаются
сразу после того, как напечатаны оба операнда.
Слово ЧИТАТЬ не должно восприниматься слишком буквально,
поскольку многие автоматы не содержат операцией ЧИТАТЬ. Например,
примитивный магазинный автомат сдвигается на входной символ,
держится на нем в течение неопределённого числа переходов, а затем
сдвигается с этого символа.
Последовательность действий ввода и вывода можно описать, не
используя слов ЧИТАТЬ и ПЕЧАТАТЬ следующим
образом:
a{a}+b{b}*c{c}{*}{+}.
Операции ввода представлены самими входными символами, а
операции вывода – символами, заключёнными в фигурные скобки. Эта
последовательность представляет собой пример того, что мы называем
«ПОСЛЕДОВАТЕЛЬНОСТЬЮ АКТОВ». Результатом указанных в ней
операций ПЕЧАТАТЬ будет выходная цепочка, состоящая из символов,
заключённых в фигурные скобки, а именно abc*+.
С целью создания математической модели перевода пару фигурных
скобок и заключённый в них выход будем рассматривать как единый
символ, называемый «СИМВОЛОМ ДЕЙСТВИЯ». Так, приведённая выше
последовательность актов содержит пять символов действия а именно {a},
{b}, {c}, {*}, {+}.
Приведённая выше последовательность актов просто говорит нам о
том, как можно обработать одно конкретное инфиксное выражение. Для
того чтобы показать, как обрабатывать все инфиксные выражения, можно
описать «множество» или «язык» последовательностей актов.
Наша цель - описание таких языков с помощью контекстносвободных грамматик.
Обычно исходным пунктом при разработке контекстно-свободного
описания языка последовательностей актов служат грамматика для
входного языка, так как она описывает входную часть последовательности
актов.
Грамматика
для
инфиксных
выражений
(с
начальным
нетерминалом <E>) такова:
Для удобства изложения грамматика содержит три конкретных
имени
переменных
последовательностей
a,
b,
актов,
c.
Чтобы
мы
построить
просто
грамматику
опишем
для
действия,
соответствующие каждой правой части правил грамматики.
Например, чтобы напечатать «a» после того, как «а» прочитано,
правило 6 изменится следующим образом: <P> -> a{a}
Чтобы напечатать знак сложения после того, как напечатаны оба его
операнда, правило 1 заменяется на: <E> ->
<E>+<T>{+}.
Это новое правило можно выразить словами «обработка <E>
состоит из обработки <E>, и чтения +, обработки <T> и печати +». После
аналогичных изменений в других правилах новая грамматика будет
таковой:
Эта новая грамматика представляет собой то, что мы называем
«ТРАНСЛИРУЮЩЕЙ
ГРАММАТИКОЙ»
или
«ГРАММАТИКОЙ
ПЕРЕВОДА».
В силу соответствия между правилами инфиксной грамматики и
правилами
транслирующей
грамматики
вывод
входной
последовательности в инфиксной грамматике можно использовать для
того, чтобы получить последовательность актов для этой входной
последовательности с помощью транслирующей грамматики.
Это делается просто путём применения соответствующих правил в
соответствующих местах. Например, инфиксное выражение (a+b)*c.
Левый вывод этой цепочки получается применением последовательности
правил 2, 3 ,4 ,5 ,1 ,2 ,4 ,6 ,4 ,7 ,8. Эта же последовательность правил
транслирующей
грамматики,
применённая
к
соответствующим
нетерминалам, даёт вывод последовательности актов для этой входной
последовательности. Соответствующие выводы имеют вид:
Некоторые более тривиальные шаги вывода здесь не приведены они
имеют место там, где появляется символ ->*.
Приведённые выше идеи мы сформулируем в виде математической
модели с помощью следующих определений.
Транслирующей грамматикой или грамматикой перевода называется
контекстно-свободная грамматика, множество терминальных символов
которой разбито на множество входных символов и множество символов
действия. Цепочки языка, определяемого транслируются грамматикой,
называются последовательностями актов.
Пример транслирующей грамматики:
Для того чтобы символы действие при чтении отличались от
нетерминальных и входных символов, мы примем соглашение заключать
их в фигурные скобки. При таком соглашении, взглянув на любую
цепочку символов, можно сразу увидеть, какие из символов являются
нетерминалами, какие - входными символами и какие - символами
действий. Например, если только что приведённая грамматика была бы
построена с символами действий {x}, {y}, {z} вместо x, y, и z, то
множество правил имело бы вид:
В многих применениях, например, при переводе из инфиксной
записи в польскую, подразумевается, что каждый символ действия
представляет
процедуру,
которая
осуществляет
выдачу
символа,
заключённого в скобки. Когда надо подчеркнуть, что подразумевается
именно
такая
интерпретация,
будем
называть
соответствующую
грамматику грамматикой, транслирующей в цепочки, или грамматикой
цепочечного
перевода.
Таким
образом,
термин
«грамматика,
транслирующая в цепочки» указывает на то, что данная транслирующая
грамматика предназначена для описания перевода входных цепочек в
выходные цепочки. В этом, заключается отличие от общей интерпретации
символов действия, как представляющих произвольные процедуры.
32) СИНТАКСИЧЕСКИ УПРАВЛЯЕМЫЙ ПЕРЕВОД
Математически мы рассматриваем «перевод» как множество пар,
где первый элемент принадлежит множеству объектов, которые надо
перевести, а второй элемент - множеству объектов, которые являются
результатом перевода. В переводах, обсуждаемых в данной разделе,
первый элемент пары - это цепочка входного языка, а второй элемент –
последовательность,
представляющая
результат
перевода
входной
цепочки. Когда эти пары получаются с помощью некоторой грамматики,
перевод называют синтаксически управляемым переводом.
В этом разделе мы используем понятие последовательности актов
как основу одного класса синтаксически управляемых переводов. Вначале
мы установим, как последовательность действий можно использовать для
задания пары.
Пусть дана последовательность актов, состоящая из входных
символов и символов действия, мы будем использовать термин «входная
последовательность»
или
«входная
последовательности
входных
цепочка»
символов,
для
обозначения
полученной
из
последовательности актов путём вычёркивания всех символов действия, и
термин
«подпоследовательность
действий»
для
обозначения
последовательности
символов
действия,
полученной
из
последовательности актов путём вычёркивания всех входных символов.
Мы говорим, что входная подпоследовательность «образует пару»
подпоследовательность действий.
При
этом
определении
для
последовательности
актов
a{a}+b{b}*c{c}{*}{+}, обсуждавшейся в прошлом разделе, входная
цепочка a+b*c образует пару с подпоследовательностью действий
{a}{b}{c}{*}{+}.
Для данной транслирующей грамматики множество пар можно
получить, образуя пары из входной подпоследовательности a+b*c каждой
последовательности
актов
и
подпоследовательности
действий
{a}{b}{c}{*}{+}. Это множество пар a{a}+b{b}*c{c}{*}{+}, называется
«переводом, определяемым данной транслирующей грамматикой».
При таком определении перевод, определяемый грамматикой
предшествующего
раздела,
транслирующей
инфиксную
запись
в
польскую, - это множество пар, в которых первый элемент - это
инфиксное выражение, а второй - последовательность символов действия,
говорящих о том, как напечатать эквивалентное выражение в польской
записи.
Здесь же мы лишь отметим, что нас интересует в основном только те
случаи, когда каждая входная подпоследовательность является частью
одной последовательности актов и, следовательно, имеет только один
перевод. Говоря о переводах, мы часто используем понятия «входной
грамматики»:
Если дана транслирующая грамматика, то грамматика, полученная
путём вычёркивания всех символов действия из правил этой грамматики,
называется входной грамматикой для этой транслирующей грамматики.
Язык,
определяемый
языком».
входной
грамматикой,
называется
«входным
В примере перевода инфиксной записи в польскую входная
грамматика
это
-
выражения.
Для
просто
грамматика,
грамматики,
определяющая
иллюстрировавшей
инфиксные
определение
транслирующей грамматики, входной грамматикой будет:
Для любой транслирующей грамматики входной язык является не
чем иным, как множеством входных цепочек. Таким образом, входная
грамматика, описывает множество цепочек, для которых транслирующая
грамматика определяет переводы.
Хотя
мы
определили
входную
грамматику,
исходя
из
транслирующей грамматики, обычно ход событий при разработке
компилятора несколько иной. Вначале обрабатываемый язык описывается
входной грамматикой, а затем в правила этой грамматики вставляются
символы действия для описания требуемого процесса обработки.
Для данной транслирующей грамматики «выходную грамматику»
или «грамматику действий» можно получить вычёркиванием входных
символов.
Таким
образом,
транслирующую
грамматику
можно
рассматривать как переводящую с одного контекстно-свободного языка,
входного языка, на другой контекстно-свободный язык выходной язык или
язык действий.
В
случае
грамматики
цепочечного
перевода
мы
считаем
последовательность символов действия синонимом соответствующей
выходной цепочки. В примере перевода и инфиксной записи в польскую
мы рассматриваем последовательность действий {a}{b}{c}{*}{+} как
синоним выходной последовательности abc*+, и любую из этих
последовательностей символов можно называть переводом входной
цепочки a+b*c. Таким образом, грамматику, транслирующую в цепочки
можно интерпретировать как способ определения перевода со входного
языка на выходной язык.
Перевод можно определить многими различными транслирующими
грамматиками. Например, перевод инфиксных выражении в польскую
запись, рассмотренный ранее, можно также описать при помощи
следующей
транслирующей
грамматики,
основанной
на
входной
грамматике рассмотренной выше:
Как было описано раннее <T> в правиле 2 порождает правый
операнд
оператора
+.
Значит,
подходящее
место
для
+
будет
непосредственно за <T>. Аналогичные соображения применимы к * в
правиле 5.
Левый
вывод
последовательности
актов
для
входной
последовательности (a+b)*c, согласно этой грамматике, будет таким:
33) НЕОДНОЗНАЧНЫЕ ГРАММАТИКИ И МНОГОЗНАЧНЫЕ
ПЕРЕВОДЫ
Как уже говорилось ранее грамматика называется неоднозначной
если в определяемом ею языке существует цепочка для которой имеется
более одного дерева вывода. Например используя грамматику
для цепочки «xtxtx» получим два дерева соответствующих левым
выводам:
на рисунке (7.12) показаны оба дерева вывода. Когда неоднозначная
грамматика
превращается
в
транслирующую
грамматику
путём
добавления в неё символов действия, некоторые входные цепочки могут
иметь более чем один перевод. В данном примере это произойдёт, если мы
добавим выходные символы «T» и «X» для получения грамматики
цепочечного перевода:
Два
приведённых
выше
вывода
порождают
разные
последовательности актов, соответствующие входной цепочке «xtxtx» а
именно:
Выходными подпоследовательностями этих последовательностей
актов будут соответственно XXTXT и XXXTT. Если интерпретировать «t»
как бинарную операцию, а «x» - как операнд, то эти выходные
подпоследовательности представляют собой два выражения в польской
записи, вычисляемые по-разному. Первый перевод вызывает вначале
выполнение левой операции, как в выражении «(xtx)xt» а второй - правой
операции, как в выражении «xt(xtx)».
С точки зрения проектирования компиляторов, неоднозначные
переводы имеют сомнительную ценность, так как для данной входной
цепочки компилятор может сделать только один перевод. Одна из
возможных интерпретаций состоит в том, что человек, задающий
неоднозначных грамматику, хочет, чтобы компилятор выдал любой
перевод. В нашем примере такая ситуация возникает, если операция «t»
является
операцией
сложения,
так
как
справедливо
сле-дующее
математическое соотношение:
Если в качестве «t» взять операцию вычитания, то такого уже не
будет, так как:
В любом случае наиболее полезное техника синтаксической
обработки
цепочек
основывается
на
использовании
однозначных
грамматик.
Для языка из текущего примера существует много однозначных
грамматик, в том числе две такие:
Первая грамматика подходит для определения переводов, в которых
вначале выполняется самая левая операция, вторая - для определения
переводов в которых раньше выполняется самая правая операция.
С математической точки зрения транслирующая грамматика может
задавать много переводов данной входной цепочки, и любую выходную
последовательность следует считать переводом соответствующей входной
цепочки.
В случае однозначной грамматики, перевод понимается однозначно.
Так как большинство грамматик, с которыми мы будем иметь дело,
однозначны, иногда мы не будем специально указывать, что перевод
однозначен.
Чтобы
показать,
неосмотрительном
как
неоднозначность
определении
языка,
может
повлиять
предположим,
что
при
была
определена грамматика, включающая следующие два правила:
где не терминал <S> порождает операторы, а не терминал <B> логические выражения. Подразумеваемая интерпретация каждого из этих
правил очевидна, однако в языке возникает двусмысленность, поскольку
существует два дерева, соответствующие выводу.
если интерпретировать оператор
согласно рисунка (7.13, а), то в качестве выхода будет напечатано
«Y» тогда как вычисление этого оператора, согласно рисунку (7.13,б), не
даёт на выходе никакого результата. Отсюда следует, что грамматику
нужно определить так, чтобы одну из этих интерпретаций исключить.
Download